From 949e90903301c377d517bb929186a1adaf17b763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Wed, 21 Jan 2026 14:15:17 +0100 Subject: [PATCH 01/11] allow customize alias source field [ci skip] --- composer.json | 12 +++---- src/Dca/AliasFieldConfiguration.php | 33 ++++++++++++++++++- .../DcaField/AliasDcaFieldListener.php | 25 ++++++++++---- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index 7b00f4c1..6d16f737 100644 --- a/composer.json +++ b/composer.json @@ -8,14 +8,14 @@ "ext-simplexml": "*", "php": "^8.1", "contao/core-bundle": "^4.13 || ^5.0", - "doctrine/dbal": "^2.13 || ^3.0", + "doctrine/dbal": "^2.13 || ^3.0 || ^4.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/config": "^5.4 || ^6.0", + "symfony/config": "^5.4 || ^6.0 || ^7.0", "symfony/event-dispatcher-contracts": "^1.0 || ^2.0 || ^3.0", - "symfony/filesystem": "^5.4 || ^6.0", - "symfony/http-foundation": "^5.4 || ^6.0", - "symfony/http-kernel": "^5.4 || ^6.0", - "symfony/string": "^5.2 || ^6.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/string": "^5.2 || ^6.0 || ^7.0", "twig/twig": "^3.0" }, "require-dev": { diff --git a/src/Dca/AliasFieldConfiguration.php b/src/Dca/AliasFieldConfiguration.php index 53a46a84..5cc4717c 100644 --- a/src/Dca/AliasFieldConfiguration.php +++ b/src/Dca/AliasFieldConfiguration.php @@ -6,24 +6,55 @@ class AliasFieldConfiguration extends DcaFieldConfiguration { + /** + * @internal + * @deprecated + */ public ?array $aliasExistCallback = [AliasDcaFieldListener::class, 'onFieldsAliasSaveCallback']; + /** + * @internal + */ public string $fieldName = 'alias'; + /** + * @internal + */ + public string $titleField = 'title'; + + /** + * @internal + */ + public ?array $generateAliasCallback = [AliasDcaFieldListener::class, 'onFieldsAliasSaveCallback']; + /** * Override the default alias exist function. Provide as [Class, 'method']. * * @param array $aliasExistCallback + * @deprecated Deprecated since version 3.10. Use setGenerateAliasCallback instead. */ public function setAliasExistCallback(?array $aliasExistCallback): AliasFieldConfiguration { - $this->aliasExistCallback = $aliasExistCallback; + $this->generateAliasCallback = $aliasExistCallback; return $this; } + /** + * Override the default alias exist function. Provide as [Class, 'method']. + * + * @param array $aliasExistCallback + */ + public function setGenerateAliasCallback(?array $aliasExistCallback): AliasFieldConfiguration {} + public function setFieldName(string $fieldName): AliasFieldConfiguration { $this->fieldName = $fieldName; return $this; } + + public function setTitleField(string $titleField): AliasFieldConfiguration + { + $this->titleField = $titleField; + return $this; + } } \ No newline at end of file diff --git a/src/EventListener/DcaField/AliasDcaFieldListener.php b/src/EventListener/DcaField/AliasDcaFieldListener.php index a8da16a7..5d3a4272 100644 --- a/src/EventListener/DcaField/AliasDcaFieldListener.php +++ b/src/EventListener/DcaField/AliasDcaFieldListener.php @@ -23,8 +23,8 @@ public function onLoadDataContainer(string $table): void $registration = AliasField::getRegistrations()[$table]; $field = AliasField::getField(); - if (is_array($registration->aliasExistCallback)) { - $field['save_callback'][] = $registration->aliasExistCallback; + if (is_array($registration->generateAliasCallback)) { + $field['save_callback'][] = $registration->generateAliasCallback; } $this->applyDefaultFieldAdjustments($field, $registration); @@ -40,16 +40,27 @@ public function onFieldsAliasSaveCallback($value, DataContainer $dc) ->execute($alias, $dc->id) ->numRows > 0); + if (method_exists($dc, 'getCurrentRecord')) { + $row = $dc->getCurrentRecord(); + } else { + /** + * Contao 4 fallback + * @todo Remove when contao 5 only + * @phpstan-ignore property.notFound + */ + $row = $dc->activeRecord->row(); + } + // Generate an alias if there is none if (!$value) { + $titleField = AliasField::getRegistrations()[$dc->table]?->titleField ?? 'title'; + $value = $this->container->get('contao.slug')->generate( - /** @phpstan-ignore property.notFound */ - (string)$dc->activeRecord->title, - /** @phpstan-ignore property.notFound */ - (int)$dc->activeRecord->pid, + (string)$row[$titleField], + (int)$row['pid'], $aliasExists ); - } elseif (preg_match('/^[1-9]\d*$/', (string) $value)) { + } elseif (preg_match('/^[1-9]\d*$/', (string)$value)) { throw new \Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasNumeric'], $value)); } elseif ($aliasExists($value)) { throw new \Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasExists'], $value)); From 2887b44cacb1b14780798cf63bfad8ff004257dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Wed, 21 Jan 2026 14:54:19 +0100 Subject: [PATCH 02/11] fix phpstan report, update rector config [ci skip] --- composer.json | 7 +++--- rector.php | 25 ++++++++++++++++--- src/Dca/AliasFieldConfiguration.php | 6 ++++- src/EntityFinder/EntityFinderHelper.php | 2 +- src/EntityFinder/Finder.php | 1 - .../DcaField/AliasDcaFieldListener.php | 5 ++-- src/Util/UserUtil.php | 2 -- 7 files changed, 34 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index 6d16f737..ac3e560f 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "ext-simplexml": "*", "php": "^8.1", "contao/core-bundle": "^4.13 || ^5.0", - "doctrine/dbal": "^2.13 || ^3.0 || ^4.0", + "doctrine/dbal": "^3.0 || ^4.0", "psr/log": "^1.0 || ^2.0 || ^3.0", "symfony/config": "^5.4 || ^6.0 || ^7.0", "symfony/event-dispatcher-contracts": "^1.0 || ^2.0 || ^3.0", @@ -27,8 +27,9 @@ "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0", "phpstan/phpstan": "^1.10 || ^2.0", "phpstan/phpstan-symfony": "^1.2 || ^2.0", - "rector/rector": "^1.2 || ^2.0", - "contao/contao-rector": "dev-main" + "rector/rector": "^1.2 || ^2.3.3", + "contao/contao-rector": "dev-main", + "contao/contao": "^4.13" }, "autoload": { "psr-4": { diff --git a/rector.php b/rector.php index 9ecf2d44..8acad045 100644 --- a/rector.php +++ b/rector.php @@ -5,23 +5,33 @@ use Contao\Rector\Set\ContaoLevelSetList; use Contao\Rector\Set\ContaoSetList; use Rector\Config\RectorConfig; +use Rector\Php81\Rector\Array_\ArrayToFirstClassCallableRector; use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector; use Rector\Set\ValueObject\LevelSetList; use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector; +use Rector\ValueObject\PhpVersion; return RectorConfig::configure() ->withPaths([ __DIR__ . '/src', ]) - ->withPhpVersion(\Rector\ValueObject\PhpVersion::PHP_84) + ->withPhpVersion(PhpVersion::PHP_84) ->withRules([ AddVoidReturnTypeWhereNoReturnRector::class, # In Vorbereitung für PHP 8.4: ExplicitNullableParamTypeRector::class ]) - ->withImportNames(importShortClasses: false, removeUnusedImports: true) - ->withComposerBased(symfony: true) + ->withImportNames( + importShortClasses: false, + removeUnusedImports: true, + ) + ->withComposerBased( + twig: true, + doctrine: true, + phpunit: true, + symfony: true, + ) ->withSets([ LevelSetList::UP_TO_PHP_81, # Erst mit Symfony 6 (Contao 5) nutzen: @@ -29,4 +39,11 @@ ContaoLevelSetList::UP_TO_CONTAO_413, ContaoSetList::FQCN, ContaoSetList::ANNOTATIONS_TO_ATTRIBUTES, - ]); \ No newline at end of file + ]) + ->withSkip([ + ArrayToFirstClassCallableRector::class, + ]) + + + + ; \ No newline at end of file diff --git a/src/Dca/AliasFieldConfiguration.php b/src/Dca/AliasFieldConfiguration.php index 5cc4717c..ca6ef2e9 100644 --- a/src/Dca/AliasFieldConfiguration.php +++ b/src/Dca/AliasFieldConfiguration.php @@ -44,7 +44,11 @@ public function setAliasExistCallback(?array $aliasExistCallback): AliasFieldCon * * @param array $aliasExistCallback */ - public function setGenerateAliasCallback(?array $aliasExistCallback): AliasFieldConfiguration {} + public function setGenerateAliasCallback(?array $aliasExistCallback): AliasFieldConfiguration + { + $this->generateAliasCallback = $aliasExistCallback; + return $this; + } public function setFieldName(string $fieldName): AliasFieldConfiguration { diff --git a/src/EntityFinder/EntityFinderHelper.php b/src/EntityFinder/EntityFinderHelper.php index 20666235..97e341f2 100644 --- a/src/EntityFinder/EntityFinderHelper.php +++ b/src/EntityFinder/EntityFinderHelper.php @@ -201,4 +201,4 @@ public function setRow(array $arrData) } }; } -} +} \ No newline at end of file diff --git a/src/EntityFinder/Finder.php b/src/EntityFinder/Finder.php index 495986b7..29bbe2e1 100644 --- a/src/EntityFinder/Finder.php +++ b/src/EntityFinder/Finder.php @@ -255,7 +255,6 @@ private function news(int $id): ?Element NewsModel::getTable(), 'News ' . $model->headline . ' (ID: ' . $model->id . ')', (function () use ($model): \Generator { - /* @phpstan-ignore class.notFound */ yield ['table' => NewsArchiveModel::getTable(), 'id' => $model->pid]; })() ); diff --git a/src/EventListener/DcaField/AliasDcaFieldListener.php b/src/EventListener/DcaField/AliasDcaFieldListener.php index 5d3a4272..095818f0 100644 --- a/src/EventListener/DcaField/AliasDcaFieldListener.php +++ b/src/EventListener/DcaField/AliasDcaFieldListener.php @@ -46,14 +46,15 @@ public function onFieldsAliasSaveCallback($value, DataContainer $dc) /** * Contao 4 fallback * @todo Remove when contao 5 only - * @phpstan-ignore property.notFound */ $row = $dc->activeRecord->row(); } // Generate an alias if there is none if (!$value) { - $titleField = AliasField::getRegistrations()[$dc->table]?->titleField ?? 'title'; + /** @var ?AliasFieldConfiguration $fieldConfiguration */ + $fieldConfiguration = AliasField::getRegistrations()[$dc->table]; + $titleField = $fieldConfiguration?->titleField ?? 'title'; $value = $this->container->get('contao.slug')->generate( (string)$row[$titleField], diff --git a/src/Util/UserUtil.php b/src/Util/UserUtil.php index 1de54fea..7176261b 100644 --- a/src/Util/UserUtil.php +++ b/src/Util/UserUtil.php @@ -16,8 +16,6 @@ use Contao\Model\Collection; use Contao\StringUtil; use Contao\UserModel; -use HeimrichHannot\UtilsBundle\Util\DatabaseUtil; -use HeimrichHannot\UtilsBundle\Util\ModelUtil; use HeimrichHannot\UtilsBundle\Util\UserUtil\UserType; class UserUtil From 963803b36e4acf0fbec4ff0e376cfc5b765074bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Wed, 21 Jan 2026 15:40:00 +0100 Subject: [PATCH 03/11] update tests and fix workflow --- .github/workflows/ci.yml | 2 + .../DcaField/AliasDcaFieldListener.php | 2 +- .../DcaField/AliasDcaFieldListenerTest.php | 123 ++++++++++-------- 3 files changed, 71 insertions(+), 56 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 037d652a..e4169fec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,8 @@ jobs: exclude: - php: 8.1 contao: 5.6.* + - php: 8.4 + contao: 4.13.* steps: diff --git a/src/EventListener/DcaField/AliasDcaFieldListener.php b/src/EventListener/DcaField/AliasDcaFieldListener.php index 095818f0..a0f69df4 100644 --- a/src/EventListener/DcaField/AliasDcaFieldListener.php +++ b/src/EventListener/DcaField/AliasDcaFieldListener.php @@ -53,7 +53,7 @@ public function onFieldsAliasSaveCallback($value, DataContainer $dc) // Generate an alias if there is none if (!$value) { /** @var ?AliasFieldConfiguration $fieldConfiguration */ - $fieldConfiguration = AliasField::getRegistrations()[$dc->table]; + $fieldConfiguration = AliasField::getRegistrations()[$dc->table] ?? null; $titleField = $fieldConfiguration?->titleField ?? 'title'; $value = $this->container->get('contao.slug')->generate( diff --git a/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php b/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php index 4bedbf86..8c99570d 100644 --- a/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php +++ b/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php @@ -76,50 +76,7 @@ public function testOnFieldsAliasSaveCallbackGeneratesAliasIfEmpty() 'container' => $container, ]); - $dc = new class () extends DataContainer - { - public int $id; - public string $table; - public object $activeRecord; - - public function __construct() - { - } - - public function __get($strKey) - { - if (isset($this->{$strKey})) { - return $this->{$strKey}; - } - - return parent::__get($strKey); - } - - public function __set($strKey, $varValue) - { - if (isset($this->{$strKey})) { - $this->{$strKey} = $varValue; - } else { - parent::__set($strKey, $varValue); - } - } - - public function getPalette() - { - // TODO: Implement getPalette() method. - } - - protected function save($varValue) - { - // TODO: Implement save() method. - } - }; - -// $dc = $this->createMock(DataContainer::class); - $dc->activeRecord = (object)['title' => 'Test', 'pid' => 1]; - $dc->table = 'tl_article'; - $dc->id = 1; - + $dc = $this->createDataContainerMock(['table' => 'tl_article', 'id' => 1, 'title' => 'Test', 'pid' => 1]); $result = $listener->onFieldsAliasSaveCallback('', $dc); $this->assertEquals('generated-alias', $result); } @@ -146,16 +103,11 @@ public function testOnFieldsAliasSaveCallbackThrowsOnNumericAlias() }); - $listener = $this->getTestInstance([ 'container' => $container, ]); - $dc = $this->createMock(DataContainer::class); - $dc->activeRecord = (object)['title' => 'Test', 'pid' => 1]; - $dc->table = 'tl_article'; - $dc->id = 1; - + $dc = $this->createDataContainerMock(['table' => 'tl_article', 'id' => 1, 'title' => 'Test', 'pid' => 1]); $GLOBALS['TL_LANG']['ERR']['aliasNumeric'] = 'Alias darf nicht numerisch sein: %s'; $listener->onFieldsAliasSaveCallback('123', $dc); @@ -197,14 +149,75 @@ public function testOnFieldsAliasSaveCallbackThrowsOnExistingAlias() 'container' => $container, ]); - $dc = $this->createMock(DataContainer::class); - $dc->activeRecord = (object)['title' => 'Test', 'pid' => 1]; - $dc->table = 'tl_article'; - $dc->id = 1; - + $dc = $this->createDataContainerMock(['table' => 'tl_article', 'id' => 1, 'title' => 'Test', 'pid' => 1]); $GLOBALS['TL_LANG']['ERR']['aliasExists'] = 'Alias existiert bereits: %s'; $listener->onFieldsAliasSaveCallback('existing-alias', $dc); } + private function createActiveRecord(array $row) + { + return new class ($row) { + + public function __construct(private array $row) {} + + public function row(): array + { + return $this->row; + } + }; + } + + private function createDataContainerMock(array $row): DataContainer + { + return new class ($row) extends DataContainer { + public int $id; + public string $table; + public object $activeRecord; + + public function __construct(array $row) + { + $this->table = $row['table']; + $this->id = $row['id']; + $this->activeRecord = new class ($row) { + + public function __construct(private array $row) {} + + public function row(): array + { + return $this->row; + } + }; + } + + public function __get($strKey) + { + if (isset($this->{$strKey})) { + return $this->{$strKey}; + } + + return parent::__get($strKey); + } + + public function __set($strKey, $varValue) + { + if (isset($this->{$strKey})) { + $this->{$strKey} = $varValue; + } else { + parent::__set($strKey, $varValue); + } + } + + public function getPalette() + { + // TODO: Implement getPalette() method. + } + + protected function save($varValue) + { + // TODO: Implement save() method. + } + }; + } + } \ No newline at end of file From 004bfce416fa48f0cde8132325cfb562acbdb716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Wed, 21 Jan 2026 15:41:32 +0100 Subject: [PATCH 04/11] allow contao 5 again --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index ac3e560f..bb5db6df 100644 --- a/composer.json +++ b/composer.json @@ -28,8 +28,7 @@ "phpstan/phpstan": "^1.10 || ^2.0", "phpstan/phpstan-symfony": "^1.2 || ^2.0", "rector/rector": "^1.2 || ^2.3.3", - "contao/contao-rector": "dev-main", - "contao/contao": "^4.13" + "contao/contao-rector": "dev-main" }, "autoload": { "psr-4": { From 6f28eb596a52dbad2da7a417efad9c7aaad42d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Wed, 21 Jan 2026 15:59:45 +0100 Subject: [PATCH 05/11] test fixes --- .../DcaField/AliasDcaFieldListenerTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php b/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php index 8c99570d..3a890de0 100644 --- a/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php +++ b/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php @@ -178,6 +178,7 @@ private function createDataContainerMock(array $row): DataContainer public function __construct(array $row) { $this->table = $row['table']; + $this->strTable = $row['table']; $this->id = $row['id']; $this->activeRecord = new class ($row) { @@ -188,6 +189,10 @@ public function row(): array return $this->row; } }; + + if (method_exists($this, 'setCurrentRecordCache')) { + static::setCurrentRecordCache($this->id, $this->table, $row); + } } public function __get($strKey) @@ -217,6 +222,17 @@ protected function save($varValue) { // TODO: Implement save() method. } + + protected static function preloadCurrentRecords(array $ids, string $table): void + { + } + + protected function denyAccessUnlessGranted($attribute, $subject): void + { + return; + } + + }; } From f0aa8214e2d1afa2aa2b055c732e7af3ea255f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Wed, 21 Jan 2026 16:05:43 +0100 Subject: [PATCH 06/11] fix test --- src/EventListener/DcaField/AliasDcaFieldListener.php | 2 +- tests/EventListener/DcaField/AliasDcaFieldListenerTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EventListener/DcaField/AliasDcaFieldListener.php b/src/EventListener/DcaField/AliasDcaFieldListener.php index a0f69df4..8b069765 100644 --- a/src/EventListener/DcaField/AliasDcaFieldListener.php +++ b/src/EventListener/DcaField/AliasDcaFieldListener.php @@ -57,7 +57,7 @@ public function onFieldsAliasSaveCallback($value, DataContainer $dc) $titleField = $fieldConfiguration?->titleField ?? 'title'; $value = $this->container->get('contao.slug')->generate( - (string)$row[$titleField], + (string)$row[$titleField] ?? '', (int)$row['pid'], $aliasExists ); diff --git a/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php b/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php index 3a890de0..b7574c6b 100644 --- a/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php +++ b/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php @@ -180,6 +180,7 @@ public function __construct(array $row) $this->table = $row['table']; $this->strTable = $row['table']; $this->id = $row['id']; + $this->intId = $row['id']; $this->activeRecord = new class ($row) { public function __construct(private array $row) {} From 0c13c39c074e4fac6e496440e7c6e47246ab0b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Wed, 21 Jan 2026 16:11:38 +0100 Subject: [PATCH 07/11] another fix --- tests/Util/ModelUtilTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Util/ModelUtilTest.php b/tests/Util/ModelUtilTest.php index 55c604a1..da352c15 100644 --- a/tests/Util/ModelUtilTest.php +++ b/tests/Util/ModelUtilTest.php @@ -196,7 +196,7 @@ public function testFindParentsRecursively() $schemaManager = $this->createMock(AbstractSchemaManager::class); $schema = $this->createMock(Schema::class); $schema->method('getTables')->willReturn([]); - $schemaManager->method('createSchema')->willReturn($schema); +// $schemaManager->method('createSchema')->willReturn($schema); $schemaManager->method('introspectSchema')->willReturn($schema); return $schemaManager; }); From 8b00189925ba05d424ba022a0ccf3f3bde39f622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Wed, 21 Jan 2026 16:20:05 +0100 Subject: [PATCH 08/11] phpstan fixes --- composer.json | 1 + src/EventListener/DcaField/AliasDcaFieldListener.php | 1 + 2 files changed, 2 insertions(+) diff --git a/composer.json b/composer.json index bb5db6df..b2b59acf 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ }, "require-dev": { "contao/manager-plugin": "^2.0", + "contao/news-bundle": "^4.13 || ^5.0", "contao/test-case": "^4.13 || ^5.0", "heimrichhannot/contao-test-utilities-bundle": "^0.1", "phpunit/phpunit": "^9.0 || ^10.0 || ^11.0", diff --git a/src/EventListener/DcaField/AliasDcaFieldListener.php b/src/EventListener/DcaField/AliasDcaFieldListener.php index 8b069765..d0edb8f1 100644 --- a/src/EventListener/DcaField/AliasDcaFieldListener.php +++ b/src/EventListener/DcaField/AliasDcaFieldListener.php @@ -46,6 +46,7 @@ public function onFieldsAliasSaveCallback($value, DataContainer $dc) /** * Contao 4 fallback * @todo Remove when contao 5 only + * @phpstan-ignore property.notFound */ $row = $dc->activeRecord->row(); } From d7e03c155f69635cc58ccb49e4bf1fb4dc1d6107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Wed, 21 Jan 2026 16:23:16 +0100 Subject: [PATCH 09/11] revert doctrine 4 support --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b2b59acf..7cc84e2a 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "ext-simplexml": "*", "php": "^8.1", "contao/core-bundle": "^4.13 || ^5.0", - "doctrine/dbal": "^3.0 || ^4.0", + "doctrine/dbal": "^3.0", "psr/log": "^1.0 || ^2.0 || ^3.0", "symfony/config": "^5.4 || ^6.0 || ^7.0", "symfony/event-dispatcher-contracts": "^1.0 || ^2.0 || ^3.0", From ccae4f674b2c0c119640654935997343833025c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Wed, 21 Jan 2026 16:42:39 +0100 Subject: [PATCH 10/11] add test for custom title field --- src/Dca/AliasFieldConfiguration.php | 13 +++-- .../DcaField/AliasDcaFieldListener.php | 2 +- .../DcaField/AliasDcaFieldListenerTest.php | 58 +++++++++++++------ 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/Dca/AliasFieldConfiguration.php b/src/Dca/AliasFieldConfiguration.php index ca6ef2e9..34cfd6e1 100644 --- a/src/Dca/AliasFieldConfiguration.php +++ b/src/Dca/AliasFieldConfiguration.php @@ -28,8 +28,6 @@ class AliasFieldConfiguration extends DcaFieldConfiguration public ?array $generateAliasCallback = [AliasDcaFieldListener::class, 'onFieldsAliasSaveCallback']; /** - * Override the default alias exist function. Provide as [Class, 'method']. - * * @param array $aliasExistCallback * @deprecated Deprecated since version 3.10. Use setGenerateAliasCallback instead. */ @@ -40,13 +38,13 @@ public function setAliasExistCallback(?array $aliasExistCallback): AliasFieldCon } /** - * Override the default alias exist function. Provide as [Class, 'method']. + * Override the default alias generation function. Provide as [Class, 'method']. * - * @param array $aliasExistCallback + * @param array $callback */ - public function setGenerateAliasCallback(?array $aliasExistCallback): AliasFieldConfiguration + public function setGenerateAliasCallback(?array $callback): AliasFieldConfiguration { - $this->generateAliasCallback = $aliasExistCallback; + $this->generateAliasCallback = $callback; return $this; } @@ -56,6 +54,9 @@ public function setFieldName(string $fieldName): AliasFieldConfiguration return $this; } + /** + * Set the field name from which the alias should be generated. + */ public function setTitleField(string $titleField): AliasFieldConfiguration { $this->titleField = $titleField; diff --git a/src/EventListener/DcaField/AliasDcaFieldListener.php b/src/EventListener/DcaField/AliasDcaFieldListener.php index d0edb8f1..3cac9145 100644 --- a/src/EventListener/DcaField/AliasDcaFieldListener.php +++ b/src/EventListener/DcaField/AliasDcaFieldListener.php @@ -48,7 +48,7 @@ public function onFieldsAliasSaveCallback($value, DataContainer $dc) * @todo Remove when contao 5 only * @phpstan-ignore property.notFound */ - $row = $dc->activeRecord->row(); + $row = $dc->activeRecord?->row() ?? []; } // Generate an alias if there is none diff --git a/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php b/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php index b7574c6b..0ee55413 100644 --- a/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php +++ b/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php @@ -2,6 +2,7 @@ namespace EventListener\DcaField; +use Ausi\SlugGenerator\SlugGenerator; use Contao\CoreBundle\Framework\ContaoFramework; use Contao\CoreBundle\Slug\Slug; use Contao\Database; @@ -41,7 +42,7 @@ public function testOnLoadDataContainer() $GLOBALS['TL_DCA']['tl_test']['fields']['alias']['save_callback'][0] ); - AliasField::register('tl_test')->setAliasExistCallback(null); + AliasField::register('tl_test')->setGenerateAliasCallback(null); $instance->onLoadDataContainer('tl_test'); $this->assertArrayHasKey('fields', $GLOBALS['TL_DCA']['tl_test']); $this->assertArrayHasKey('alias', $GLOBALS['TL_DCA']['tl_test']['fields']); @@ -50,6 +51,44 @@ public function testOnLoadDataContainer() ); } + public function testCustomTitleField() + { + $slug = $this->createMock(Slug::class); + $slug->expects($this->once()) + ->method('generate') + ->willReturnCallback(function ($value) { + return (new SlugGenerator())->generate($value); + }); + + $framework = $this->createMock(ContaoFramework::class); + + $container = $this->createMock(ContainerInterface::class); + $container->method('get')->willReturnCallback(function (string $id) use ($slug, $framework) { + switch ($id) { + case 'contao.slug': + case Slug::class: + return $slug; + case 'contao.framework': + return $framework; + default: + throw new \InvalidArgumentException("Unknown service: $id"); + } + }); + + $listener = $this->getTestInstance([ + 'container' => $container, + ]); + + AliasField::register('tl_test') + ->setTitleField('name'); + $this->assertSame( + 'test-name', $listener->onFieldsAliasSaveCallback( + '', + $this->createDataContainerMock(['table' => 'tl_test', 'id' => 1, 'name' => 'Test Name', 'pid' => 1]) + ) + ); + } + public function testOnFieldsAliasSaveCallbackGeneratesAliasIfEmpty() { $slug = $this->createMock(Slug::class); @@ -155,19 +194,6 @@ public function testOnFieldsAliasSaveCallbackThrowsOnExistingAlias() $listener->onFieldsAliasSaveCallback('existing-alias', $dc); } - private function createActiveRecord(array $row) - { - return new class ($row) { - - public function __construct(private array $row) {} - - public function row(): array - { - return $this->row; - } - }; - } - private function createDataContainerMock(array $row): DataContainer { return new class ($row) extends DataContainer { @@ -224,9 +250,7 @@ protected function save($varValue) // TODO: Implement save() method. } - protected static function preloadCurrentRecords(array $ids, string $table): void - { - } + protected static function preloadCurrentRecords(array $ids, string $table): void {} protected function denyAccessUnlessGranted($attribute, $subject): void { From 3327f4003ea14eefd5e7f1a0621b23f6dea0f382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Wed, 21 Jan 2026 16:49:48 +0100 Subject: [PATCH 11/11] raise code coverage --- src/Dca/AliasFieldConfiguration.php | 1 + .../DcaField/AliasDcaFieldListenerTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/Dca/AliasFieldConfiguration.php b/src/Dca/AliasFieldConfiguration.php index 34cfd6e1..9feadb48 100644 --- a/src/Dca/AliasFieldConfiguration.php +++ b/src/Dca/AliasFieldConfiguration.php @@ -30,6 +30,7 @@ class AliasFieldConfiguration extends DcaFieldConfiguration /** * @param array $aliasExistCallback * @deprecated Deprecated since version 3.10. Use setGenerateAliasCallback instead. + * @codeCoverageIgnore */ public function setAliasExistCallback(?array $aliasExistCallback): AliasFieldConfiguration { diff --git a/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php b/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php index 0ee55413..bb99e472 100644 --- a/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php +++ b/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php @@ -89,6 +89,20 @@ public function testCustomTitleField() ); } + public function testCustomFieldName() + { + $instance = $this->getTestInstance(); + AliasField::register('tl_test') + ->setFieldName('customAlias'); + $instance->onLoadDataContainer('tl_test'); + $this->assertArrayHasKey('fields', $GLOBALS['TL_DCA']['tl_test']); + $this->assertArrayHasKey('customAlias', $GLOBALS['TL_DCA']['tl_test']['fields']); + $this->assertSame( + [AliasDcaFieldListener::class, 'onFieldsAliasSaveCallback'], + $GLOBALS['TL_DCA']['tl_test']['fields']['customAlias']['save_callback'][0] + ); + } + public function testOnFieldsAliasSaveCallbackGeneratesAliasIfEmpty() { $slug = $this->createMock(Slug::class);