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/composer.json b/composer.json index 7b00f4c1..7cc84e2a 100644 --- a/composer.json +++ b/composer.json @@ -8,18 +8,19 @@ "ext-simplexml": "*", "php": "^8.1", "contao/core-bundle": "^4.13 || ^5.0", - "doctrine/dbal": "^2.13 || ^3.0", + "doctrine/dbal": "^3.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": { "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", @@ -27,7 +28,7 @@ "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", + "rector/rector": "^1.2 || ^2.3.3", "contao/contao-rector": "dev-main" }, "autoload": { 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 53a46a84..9feadb48 100644 --- a/src/Dca/AliasFieldConfiguration.php +++ b/src/Dca/AliasFieldConfiguration.php @@ -6,18 +6,46 @@ class AliasFieldConfiguration extends DcaFieldConfiguration { + /** + * @internal + * @deprecated + */ public ?array $aliasExistCallback = [AliasDcaFieldListener::class, 'onFieldsAliasSaveCallback']; + /** + * @internal + */ public string $fieldName = 'alias'; /** - * Override the default alias exist function. Provide as [Class, 'method']. - * + * @internal + */ + public string $titleField = 'title'; + + /** + * @internal + */ + public ?array $generateAliasCallback = [AliasDcaFieldListener::class, 'onFieldsAliasSaveCallback']; + + /** * @param array $aliasExistCallback + * @deprecated Deprecated since version 3.10. Use setGenerateAliasCallback instead. + * @codeCoverageIgnore */ public function setAliasExistCallback(?array $aliasExistCallback): AliasFieldConfiguration { - $this->aliasExistCallback = $aliasExistCallback; + $this->generateAliasCallback = $aliasExistCallback; + return $this; + } + + /** + * Override the default alias generation function. Provide as [Class, 'method']. + * + * @param array $callback + */ + public function setGenerateAliasCallback(?array $callback): AliasFieldConfiguration + { + $this->generateAliasCallback = $callback; return $this; } @@ -26,4 +54,13 @@ public function setFieldName(string $fieldName): AliasFieldConfiguration $this->fieldName = $fieldName; return $this; } + + /** + * Set the field name from which the alias should be generated. + */ + public function setTitleField(string $titleField): AliasFieldConfiguration + { + $this->titleField = $titleField; + return $this; + } } \ No newline at end of file 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 a8da16a7..3cac9145 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,29 @@ 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) { + /** @var ?AliasFieldConfiguration $fieldConfiguration */ + $fieldConfiguration = AliasField::getRegistrations()[$dc->table] ?? null; + $titleField = $fieldConfiguration?->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)); 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 diff --git a/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php b/tests/EventListener/DcaField/AliasDcaFieldListenerTest.php index 4bedbf86..bb99e472 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,12 +51,14 @@ public function testOnLoadDataContainer() ); } - public function testOnFieldsAliasSaveCallbackGeneratesAliasIfEmpty() + public function testCustomTitleField() { $slug = $this->createMock(Slug::class); $slug->expects($this->once()) ->method('generate') - ->willReturn('generated-alias'); + ->willReturnCallback(function ($value) { + return (new SlugGenerator())->generate($value); + }); $framework = $this->createMock(ContaoFramework::class); @@ -76,50 +79,57 @@ 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}; - } + AliasField::register('tl_test') + ->setTitleField('name'); + $this->assertSame( + 'test-name', $listener->onFieldsAliasSaveCallback( + '', + $this->createDataContainerMock(['table' => 'tl_test', 'id' => 1, 'name' => 'Test Name', 'pid' => 1]) + ) + ); + } - return parent::__get($strKey); - } + 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 __set($strKey, $varValue) - { - if (isset($this->{$strKey})) { - $this->{$strKey} = $varValue; - } else { - parent::__set($strKey, $varValue); - } - } + public function testOnFieldsAliasSaveCallbackGeneratesAliasIfEmpty() + { + $slug = $this->createMock(Slug::class); + $slug->expects($this->once()) + ->method('generate') + ->willReturn('generated-alias'); - public function getPalette() - { - // TODO: Implement getPalette() method. - } + $framework = $this->createMock(ContaoFramework::class); - protected function save($varValue) - { - // TODO: Implement save() method. + $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"); } - }; + }); -// $dc = $this->createMock(DataContainer::class); - $dc->activeRecord = (object)['title' => 'Test', 'pid' => 1]; - $dc->table = 'tl_article'; - $dc->id = 1; + $listener = $this->getTestInstance([ + 'container' => $container, + ]); + $dc = $this->createDataContainerMock(['table' => 'tl_article', 'id' => 1, 'title' => 'Test', 'pid' => 1]); $result = $listener->onFieldsAliasSaveCallback('', $dc); $this->assertEquals('generated-alias', $result); } @@ -146,16 +156,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 +202,77 @@ 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 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->strTable = $row['table']; + $this->id = $row['id']; + $this->intId = $row['id']; + $this->activeRecord = new class ($row) { + + public function __construct(private array $row) {} + + public function row(): array + { + return $this->row; + } + }; + + if (method_exists($this, 'setCurrentRecordCache')) { + static::setCurrentRecordCache($this->id, $this->table, $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. + } + + protected static function preloadCurrentRecords(array $ids, string $table): void {} + + protected function denyAccessUnlessGranted($attribute, $subject): void + { + return; + } + + + }; + } + } \ No newline at end of file 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; });