From 65e06841104a4ef6d33d2d55f463b87a62eded05 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Mon, 20 Oct 2025 07:33:59 +0200 Subject: [PATCH 1/4] !!!|Raise dependencies to ease maintenance --- .github/workflows/ci.yaml | 65 -------------------------- CHANGELOG.md | 8 ++++ Tests/Functional/AssertTest.php | 53 +++++++-------------- Tests/Functional/Converter/CsvTest.php | 26 +++++------ Tests/Functional/Converter/XmlTest.php | 26 +++++------ Tests/Functional/ImportTest.php | 33 ++++++------- composer.json | 12 ++--- 7 files changed, 67 insertions(+), 156 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 87cfb4f..ac706be 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -29,11 +29,6 @@ jobs: strategy: matrix: php-version: - - 7.2 - - 7.3 - - 7.4 - - 8.0 - - 8.1 - 8.2 - 8.3 - 8.4 @@ -102,42 +97,6 @@ jobs: strategy: matrix: include: - - db-version: '5.7' - php-version: '7.2' - typo3-version: '^10.4' - - db-version: '5.7' - php-version: '7.3' - typo3-version: '^10.4' - - db-version: '8' - php-version: '7.4' - typo3-version: '^10.4' - - db-version: '8' - php-version: '7.4' - typo3-version: '^11.5' - - db-version: '8' - php-version: '8.0' - typo3-version: '^11.5' - - db-version: '8' - php-version: '8.1' - typo3-version: '^11.5' - - db-version: '8' - php-version: '8.2' - typo3-version: '^11.5' - - db-version: '8' - php-version: '8.3' - typo3-version: '^11.5' - - db-version: '8' - php-version: '8.1' - typo3-version: '^12.4' - - db-version: '8' - php-version: '8.2' - typo3-version: '^12.4' - - db-version: '8' - php-version: '8.3' - typo3-version: '^12.4' - - db-version: '8' - php-version: '8.4' - typo3-version: '^12.4' - db-version: '8' php-version: '8.2' typo3-version: '^13.0' @@ -189,30 +148,6 @@ jobs: strategy: matrix: include: - - php-version: '7.2' - typo3-version: '^10.4' - - php-version: '7.3' - typo3-version: '^10.4' - - php-version: '7.4' - typo3-version: '^10.4' - - php-version: '7.4' - typo3-version: '^11.5' - - php-version: '8.0' - typo3-version: '^11.5' - - php-version: '8.1' - typo3-version: '^11.5' - - php-version: '8.2' - typo3-version: '^11.5' - - php-version: '8.3' - typo3-version: '^11.5' - - php-version: '8.1' - typo3-version: '^12.4' - - php-version: '8.2' - typo3-version: '^12.4' - - php-version: '8.3' - typo3-version: '^12.4' - - php-version: '8.4' - typo3-version: '^12.4' - php-version: '8.2' typo3-version: '^13.4' - php-version: '8.3' diff --git a/CHANGELOG.md b/CHANGELOG.md index dd89006..2233ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v2.0.0 - 2025-10-20 + +### BREAKING + +- Remove support for older dependencies. + The older versions of this package should work just fine. + There is not much more to this package, so no need to stay compatible with all versions. + ## v1.6.0 - 2025-03-04 ### Added diff --git a/Tests/Functional/AssertTest.php b/Tests/Functional/AssertTest.php index 52a461d..6cb515e 100644 --- a/Tests/Functional/AssertTest.php +++ b/Tests/Functional/AssertTest.php @@ -23,55 +23,48 @@ namespace Codappix\Typo3PhpDatasets\Tests\Functional; +use Codappix\Typo3PhpDatasets\PhpDataSet; +use Codappix\Typo3PhpDatasets\TestingFramework; use InvalidArgumentException; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; -/** - * @covers \Codappix\Typo3PhpDatasets\PhpDataSet - * @covers \Codappix\Typo3PhpDatasets\TestingFramework - * @testdox The Testing Framework trait - */ +#[CoversClass(PhpDataSet::class)] +#[CoversClass(TestingFramework::class)] +#[TestDox('The Testing Framework trait')] class AssertTest extends AbstractFunctionalTestCase { - /** - * @test - */ + #[Test] public function canAssertAgainstSimpleSet(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/SimpleSet.php'); $this->assertPHPDataSet(__DIR__ . '/Fixtures/SimpleSet.php'); } - /** - * @test - */ + #[Test] public function canAssertAgainstNullValue(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/WithNull.php'); $this->assertPHPDataSet(__DIR__ . '/Fixtures/WithNull.php'); } - /** - * @test - */ + #[Test] public function canAssertAgainstDifferentSetOfColumns(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/WithDifferentColumns.php'); $this->assertPHPDataSet(__DIR__ . '/Fixtures/WithDifferentColumns.php'); } - /** - * @test - */ + #[Test] public function canAssertMmRelation(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/MmRelation.php'); $this->assertPHPDataSet(__DIR__ . '/Fixtures/MmRelation.php'); } - /** - * @test - */ + #[Test] public function failsForMissingAssertionWithUid(): void { $this->expectException(AssertionFailedError::class); @@ -79,9 +72,7 @@ public function failsForMissingAssertionWithUid(): void $this->assertPHPDataSet(__DIR__ . '/Fixtures/AssertSimpleMissingUidSet.php'); } - /** - * @test - */ + #[Test] public function failsForDifferingAssertionWithUid(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/SimpleSet.php'); @@ -95,9 +86,7 @@ public function failsForDifferingAssertionWithUid(): void $this->assertPHPDataSet(__DIR__ . '/Fixtures/AssertDifferingWithUid.php'); } - /** - * @test - */ + #[Test] public function failsForAssertionWithoutUid(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/SimpleSet.php'); @@ -114,9 +103,7 @@ public function failsForAssertionWithoutUid(): void $this->assertPHPDataSet(__DIR__ . '/Fixtures/AssertDifferingWithoutUid.php'); } - /** - * @test - */ + #[Test] public function failsForAssertionForMmRelation(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/MmRelation.php'); @@ -138,9 +125,7 @@ public function failsForAssertionForMmRelation(): void $this->assertPHPDataSet(__DIR__ . '/Fixtures/MmRelationBroken.php'); } - /** - * @test - */ + #[Test] public function failsForAdditionalNoneAssertedRecords(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/WithDifferentColumns.php'); @@ -150,9 +135,7 @@ public function failsForAdditionalNoneAssertedRecords(): void $this->assertPHPDataSet(__DIR__ . '/Fixtures/AssertAdditionalRecords.php'); } - /** - * @test - */ + #[Test] public function throwsExceptionIfFileDoesNotExist(): void { $this->expectException(InvalidArgumentException::class); diff --git a/Tests/Functional/Converter/CsvTest.php b/Tests/Functional/Converter/CsvTest.php index 38d74e4..989ad8c 100644 --- a/Tests/Functional/Converter/CsvTest.php +++ b/Tests/Functional/Converter/CsvTest.php @@ -26,12 +26,14 @@ use Codappix\Typo3PhpDatasets\Converter\Csv; use GlobIterator; use InvalidArgumentException; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; -/** - * @covers \Codappix\Typo3PhpDatasets\Converter\Csv - * @testdox The CSV converter - */ +#[CoversClass(Csv::class)] +#[TestDox('The CSV converter')] class CsvTest extends TestCase { protected function tearDown(): void @@ -44,9 +46,7 @@ protected function tearDown(): void parent::tearDown(); } - /** - * @test - */ + #[Test] public function canBeCreated(): void { $subject = new Csv(); @@ -57,9 +57,7 @@ public function canBeCreated(): void ); } - /** - * @test - */ + #[Test] public function throwsExceptionForNoneExistingFile(): void { $subject = new Csv(); @@ -69,11 +67,9 @@ public function throwsExceptionForNoneExistingFile(): void $subject->convert('NoneExistingFile.csv'); } - /** - * @test - * @dataProvider possibleCsvFiles - * @testdox Converts $_dataName CSV to PHP - */ + #[Test] + #[TestDox('Converts $_dataName CSV to PHP')] + #[DataProvider('possibleCsvFiles')] public function convertsCsvFileToPhpFile( string $incomingCsvFile, string $expectedResultFile diff --git a/Tests/Functional/Converter/XmlTest.php b/Tests/Functional/Converter/XmlTest.php index 769ef1f..432aba8 100644 --- a/Tests/Functional/Converter/XmlTest.php +++ b/Tests/Functional/Converter/XmlTest.php @@ -26,12 +26,14 @@ use Codappix\Typo3PhpDatasets\Converter\Xml; use GlobIterator; use InvalidArgumentException; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; -/** - * @covers \Codappix\Typo3PhpDatasets\Converter\Xml - * @testdox The XML converter - */ +#[CoversClass(Xml::class)] +#[TestDox('The XML converter')] class XmlTest extends TestCase { protected function tearDown(): void @@ -44,9 +46,7 @@ protected function tearDown(): void parent::tearDown(); } - /** - * @test - */ + #[Test] public function canBeCreated(): void { $subject = new Xml(); @@ -57,9 +57,7 @@ public function canBeCreated(): void ); } - /** - * @test - */ + #[Test] public function throwsExceptionForNoneExistingFile(): void { $subject = new Xml(); @@ -69,11 +67,9 @@ public function throwsExceptionForNoneExistingFile(): void $subject->convert('NoneExistingFile.xml'); } - /** - * @test - * @dataProvider possibleXmlFiles - * @testdox Converts $_dataName XML to PHP - */ + #[Test] + #[TestDox('Converts $_dataName XML to PHP')] + #[DataProvider('possibleXmlFiles')] public function convertsXmlFileToPhpFile( string $incomingXmlFile, string $expectedResultFile diff --git a/Tests/Functional/ImportTest.php b/Tests/Functional/ImportTest.php index 3a4b8fc..3fba3ff 100644 --- a/Tests/Functional/ImportTest.php +++ b/Tests/Functional/ImportTest.php @@ -23,19 +23,20 @@ namespace Codappix\Typo3PhpDatasets\Tests\Functional; +use Codappix\Typo3PhpDatasets\PhpDataSet; +use Codappix\Typo3PhpDatasets\TestingFramework; use InvalidArgumentException; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; -/** - * @covers \Codappix\Typo3PhpDatasets\PhpDataSet - * @covers \Codappix\Typo3PhpDatasets\TestingFramework - * @testdox The Testing Framework trait - */ +#[CoversClass(PhpDataSet::class)] +#[CoversClass(TestingFramework::class)] +#[TestDox('The Testing Framework trait')] class ImportTest extends AbstractFunctionalTestCase { - /** - * @test - */ + #[Test] public function canImportSimpleSet(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/SimpleSet.php'); @@ -47,9 +48,7 @@ public function canImportSimpleSet(): void self::assertSame('Some text', $records[1]['description']); } - /** - * @test - */ + #[Test] public function canImportWithNullValue(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/WithNull.php'); @@ -58,9 +57,7 @@ public function canImportWithNullValue(): void self::assertNull($records[1]['description']); } - /** - * @test - */ + #[Test] public function canImportRecordsWithDifferentSetOfColumns(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/WithDifferentColumns.php'); @@ -73,9 +70,7 @@ public function canImportRecordsWithDifferentSetOfColumns(): void self::assertSame('Some other text', $records[2]['description']); } - /** - * @test - */ + #[Test] public function failsIfSqlError(): void { $this->expectException(AssertionFailedError::class); @@ -87,9 +82,7 @@ public function failsIfSqlError(): void $this->importPHPDataSet(__DIR__ . '/Fixtures/WithBrokenSql.php'); } - /** - * @test - */ + #[Test] public function throwsExceptionIfFileDoesNotExist(): void { $this->expectException(InvalidArgumentException::class); diff --git a/composer.json b/composer.json index 39bfcc0..c11e23c 100644 --- a/composer.json +++ b/composer.json @@ -27,18 +27,18 @@ "bin/typo3-php-datasets" ], "require": { - "php": "^7.2 || ^7.3 || ^7.4 || ^8.0 || ^8.1 || ^8.2 || ^8.3 || ^8.4", + "php": "^8.2 || ^8.3 || ^8.4", "composer-runtime-api": "^2.2", - "doctrine/dbal": "^2.13 || ^3.6 || ^4.0 || 4.0.0-RC2", - "symfony/console": "^5.4 || ^6.2 || ^7.0", - "typo3/cms-core": "^10.4 || ^11.5 || ^12.4 || ^13.0" + "doctrine/dbal": "^4.3.3", + "symfony/console": "^7.2", + "typo3/cms-core": "^13.4.19" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.4", "phpstan/phpstan": "^1.10", "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.6 || ^10.0", - "typo3/testing-framework": "^6.16 || ^7.0 || ^8.0" + "phpunit/phpunit": "^11.5 || ^12.2", + "typo3/testing-framework": "^9.2" }, "extra": { "typo3/cms": { From 087e0f05c49dbcd57cfd604924f837130a35dd42 Mon Sep 17 00:00:00 2001 From: Justus Moroni Date: Fri, 17 Oct 2025 12:55:41 +0200 Subject: [PATCH 2/4] Use public API instead of `@internal` We wrap the foreign APIs in our own classes to ease maintenance. The new classes will now use public APIs only, to prevent broken state in minor updates and properly follow semantic versioning. Resolves: #20 --- CHANGELOG.md | 7 +++++ Classes/Connection.php | 53 +++++++++++++++++++++++++++++++ Classes/ConnectionFactory.php | 37 ++++++++++++++++++++++ Classes/PhpDataSet.php | 38 +++++++--------------- Classes/Table.php | 56 +++++++++++++++++++++++++++++++++ Classes/TestingFramework.php | 6 +++- Tests/Functional/ImportTest.php | 2 +- 7 files changed, 171 insertions(+), 28 deletions(-) create mode 100644 Classes/Connection.php create mode 100644 Classes/ConnectionFactory.php create mode 100644 Classes/Table.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2233ab4..e8ef1a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## v2.0.0 - 2025-10-20 +### Added + +- Support for TYPO3 13.4.19. + They modified `@internal` API and it is the fault of this package to use the API. + We therefore now move to low level public API of doctrine/dbal instead of TYPO3 internal API. + We also encapsulate the access to those APIs for easier maintenance. + ### BREAKING - Remove support for older dependencies. diff --git a/Classes/Connection.php b/Classes/Connection.php new file mode 100644 index 0000000..74a78d0 --- /dev/null +++ b/Classes/Connection.php @@ -0,0 +1,53 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace Codappix\Typo3PhpDatasets; + +use TYPO3\CMS\Core\Database\Connection as Typo3Connection; + +/** + * @internal + */ +final readonly class Connection +{ + public function __construct( + private Typo3Connection $typo3Connection, + ) { + } + + public function insert(string $tableName, array $record, array $types): void + { + $this->typo3Connection->insert($tableName, $record, $types); + } + + public function getTable(string $tableName): Table + { + foreach ($this->typo3Connection->createSchemaManager()->listTables() as $table) { + if ($table->getObjectName()->toString() === $tableName) { + return new Table($table); + } + } + + throw new \RuntimeException('Could not fetch table details for table: ' . $tableName, 1760939710); + } +} diff --git a/Classes/ConnectionFactory.php b/Classes/ConnectionFactory.php new file mode 100644 index 0000000..42b5560 --- /dev/null +++ b/Classes/ConnectionFactory.php @@ -0,0 +1,37 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace Codappix\Typo3PhpDatasets; + +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +final readonly class ConnectionFactory +{ + public function createForTable(string $tableName): Connection + { + return new Connection( + GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName) + ); + } +} diff --git a/Classes/PhpDataSet.php b/Classes/PhpDataSet.php index a8ad5cb..bb38f61 100644 --- a/Classes/PhpDataSet.php +++ b/Classes/PhpDataSet.php @@ -23,40 +23,26 @@ namespace Codappix\Typo3PhpDatasets; -use RuntimeException; -use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Utility\GeneralUtility; - +/** + * @api + */ class PhpDataSet { + /** + * @api + */ public function import(array $dataSet): void { - foreach ($dataSet as $tableName => $records) { - $connection = $this->getConnectionPool()->getConnectionForTable($tableName); - - if (method_exists($connection, 'getSchemaManager')) { - // <= 12 - $tableDetails = $connection->getSchemaManager()->listTableDetails($tableName); - } elseif (method_exists($connection, 'getSchemaInformation')) { - // >= 13 - $tableDetails = $connection->getSchemaInformation()->introspectTable($tableName); - } else { - throw new RuntimeException('Could not check the schema for table: ' . $tableName, 1707144020); - } + $connectionFactory = new ConnectionFactory(); - foreach ($records as $record) { - $types = []; - foreach (array_keys($record) as $columnName) { - $types[] = $tableDetails->getColumn((string)$columnName)->getType()->getBindingType(); - } + foreach ($dataSet as $tableName => $records) { + $connection = $connectionFactory->createForTable($tableName); + $table = $connection->getTable($tableName); + foreach ($records as $index => $record) { + $types = array_map($table->getTypeForColumn(...), array_keys($record)); $connection->insert($tableName, $record, $types); } } } - - private function getConnectionPool(): ConnectionPool - { - return GeneralUtility::makeInstance(ConnectionPool::class); - } } diff --git a/Classes/Table.php b/Classes/Table.php new file mode 100644 index 0000000..7fa670e --- /dev/null +++ b/Classes/Table.php @@ -0,0 +1,56 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace Codappix\Typo3PhpDatasets; + +use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\Schema\Exception\ColumnDoesNotExist; +use Doctrine\DBAL\Schema\Table as DbalTable; + +/** + * @internal + */ +final readonly class Table +{ + public function __construct( + private DbalTable $dbalTable, + ) { + } + + public function getTypeForColumn(string $columnName): ParameterType + { + try { + return $this->dbalTable + ->getColumn((string)$columnName) + ->getType() + ->getBindingType() + ; + } catch (ColumnDoesNotExist $e) { + throw new \RuntimeException(sprintf( + 'Column "%s" does not exist in table: %s', + $columnName, + $this->dbalTable->getName(), + ), 1760941225); + } + } +} diff --git a/Classes/TestingFramework.php b/Classes/TestingFramework.php index fc81b83..8ddfc40 100644 --- a/Classes/TestingFramework.php +++ b/Classes/TestingFramework.php @@ -27,10 +27,13 @@ use InvalidArgumentException; /** - * Only use this within an FunctionalTestCase. + * @api Only use within `TYPO3\TestingFramework\Core\Functional\FunctionalTestCase` */ trait TestingFramework { + /** + * @api + */ protected function importPHPDataSet(string $filePath): void { $this->ensureFileExists($filePath); @@ -45,6 +48,7 @@ protected function importPHPDataSet(string $filePath): void /** * Highly inspired by TYPO3 testing framework. + * @api */ protected function assertPHPDataSet(string $filePath): void { diff --git a/Tests/Functional/ImportTest.php b/Tests/Functional/ImportTest.php index 3fba3ff..09848f5 100644 --- a/Tests/Functional/ImportTest.php +++ b/Tests/Functional/ImportTest.php @@ -77,7 +77,7 @@ public function failsIfSqlError(): void $this->expectExceptionMessageMatches( '#Error for PHP data-set "' . __DIR__ . '/Fixtures/WithBrokenSql.php":' . PHP_EOL - . 'There is no column with name .*none_existing_column.* on table .*pages.*\.#' + . 'Column "none_existing_column" does not exist in table: pages#' ); $this->importPHPDataSet(__DIR__ . '/Fixtures/WithBrokenSql.php'); } From 71daf73d7f5cc1a8f6f20c17d622202de411fd83 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Mon, 20 Oct 2025 08:45:45 +0200 Subject: [PATCH 3/4] Increase and add dev dependencies This mostly involved PHPStan and pushes us to fix some PHPStan findings. --- .gitattributes | 1 + CHANGELOG.md | 3 + Classes/Command/ConvertFromCsv.php | 7 +- Classes/Command/ConvertFromXml.php | 7 +- Classes/Connection.php | 10 ++- Classes/Converter/Xml.php | 9 --- Classes/PhpDataSet.php | 1 + Classes/Table.php | 3 +- Classes/TestingFramework.php | 101 ++++++++++++++++++------- Tests/Functional/AssertTest.php | 12 ++- Tests/Functional/Converter/CsvTest.php | 14 +--- Tests/Functional/Converter/XmlTest.php | 14 +--- Tests/Functional/ImportTest.php | 1 + composer.json | 8 +- phpstan-baseline.neon | 47 +++++++++++- phpstan.neon | 21 +++-- 16 files changed, 184 insertions(+), 75 deletions(-) diff --git a/.gitattributes b/.gitattributes index 2c1bae6..36c4f25 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,7 @@ Tests export-ignore .php-cs-fixer.dist.php export-ignore phpstan.neon export-ignore +phpstan-baseline.neon export-ignore phpunit.xml.dist export-ignore shell.nix export-ignore diff --git a/CHANGELOG.md b/CHANGELOG.md index e8ef1a6..a057814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ They modified `@internal` API and it is the fault of this package to use the API. We therefore now move to low level public API of doctrine/dbal instead of TYPO3 internal API. We also encapsulate the access to those APIs for easier maintenance. + We also raise dev dependencies to have automated CI verification that we do not use internal APIs anymore. +- Proper errors if a record was found in DB, but not within assertions. +- Do not include `phpstan-baseline.neon` in git exports / distribution. ### BREAKING diff --git a/Classes/Command/ConvertFromCsv.php b/Classes/Command/ConvertFromCsv.php index 544ea36..0dd0d36 100644 --- a/Classes/Command/ConvertFromCsv.php +++ b/Classes/Command/ConvertFromCsv.php @@ -55,7 +55,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $converter = new Csv(); - foreach ($files as $file) { + foreach ($files as $index => $file) { + if (is_string($file) === false) { + $output->writeln(sprintf('File at index "%s" needs to be a string.', $index)); + return Command::INVALID; + } + try { $converter->convert(realpath($file) ?: $file); } catch (\Exception $e) { diff --git a/Classes/Command/ConvertFromXml.php b/Classes/Command/ConvertFromXml.php index 57fb5b7..4b62a9c 100644 --- a/Classes/Command/ConvertFromXml.php +++ b/Classes/Command/ConvertFromXml.php @@ -55,7 +55,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $converter = new Xml(); - foreach ($files as $file) { + foreach ($files as $index => $file) { + if (is_string($file) === false) { + $output->writeln(sprintf('File at index "%s" needs to be a string.', $index)); + return Command::INVALID; + } + try { $converter->convert(realpath($file) ?: $file); } catch (\Exception $e) { diff --git a/Classes/Connection.php b/Classes/Connection.php index 74a78d0..eef726c 100644 --- a/Classes/Connection.php +++ b/Classes/Connection.php @@ -23,6 +23,7 @@ namespace Codappix\Typo3PhpDatasets; +use Doctrine\DBAL\ParameterType; use TYPO3\CMS\Core\Database\Connection as Typo3Connection; /** @@ -35,6 +36,10 @@ public function __construct( ) { } + /** + * @param string[] $record + * @param ParameterType[] $types + */ public function insert(string $tableName, array $record, array $types): void { $this->typo3Connection->insert($tableName, $record, $types); @@ -44,7 +49,10 @@ public function getTable(string $tableName): Table { foreach ($this->typo3Connection->createSchemaManager()->listTables() as $table) { if ($table->getObjectName()->toString() === $tableName) { - return new Table($table); + return new Table( + $tableName, + $table + ); } } diff --git a/Classes/Converter/Xml.php b/Classes/Converter/Xml.php index 2f4baa7..d8f1e62 100644 --- a/Classes/Converter/Xml.php +++ b/Classes/Converter/Xml.php @@ -24,7 +24,6 @@ namespace Codappix\Typo3PhpDatasets\Converter; use InvalidArgumentException; -use SimpleXMLElement; use SplFileObject; class Xml implements Converter @@ -71,16 +70,8 @@ private function buildContent(string $xmlContent): string throw new \Exception('Could not parse XML content.', 1681287859); } foreach ($xml->children() as $table) { - if (!$table instanceof SimpleXMLElement) { - continue; - } - $insertArray = []; foreach ($table->children() as $column) { - if (!$column instanceof SimpleXMLElement) { - continue; - } - $columnName = $column->getName(); $columnValue = (string)$table->$columnName; diff --git a/Classes/PhpDataSet.php b/Classes/PhpDataSet.php index bb38f61..11903b2 100644 --- a/Classes/PhpDataSet.php +++ b/Classes/PhpDataSet.php @@ -30,6 +30,7 @@ class PhpDataSet { /** * @api + * @param array[]> $dataSet */ public function import(array $dataSet): void { diff --git a/Classes/Table.php b/Classes/Table.php index 7fa670e..0fc7b7e 100644 --- a/Classes/Table.php +++ b/Classes/Table.php @@ -33,6 +33,7 @@ final readonly class Table { public function __construct( + private string $tableName, private DbalTable $dbalTable, ) { } @@ -49,7 +50,7 @@ public function getTypeForColumn(string $columnName): ParameterType throw new \RuntimeException(sprintf( 'Column "%s" does not exist in table: %s', $columnName, - $this->dbalTable->getName(), + $this->tableName, ), 1760941225); } } diff --git a/Classes/TestingFramework.php b/Classes/TestingFramework.php index 8ddfc40..60b1823 100644 --- a/Classes/TestingFramework.php +++ b/Classes/TestingFramework.php @@ -36,9 +36,7 @@ trait TestingFramework */ protected function importPHPDataSet(string $filePath): void { - $this->ensureFileExists($filePath); - - $dataSet = include $filePath; + $dataSet = $this->getDataSet($filePath); try { (new PhpDataSet())->import($dataSet); } catch (Exception $e) { @@ -52,57 +50,106 @@ protected function importPHPDataSet(string $filePath): void */ protected function assertPHPDataSet(string $filePath): void { - $this->ensureFileExists($filePath); + if (is_array($GLOBALS['TCA'] ?? null) === false) { + throw new \RuntimeException('TYPO3 GLOBALS["TCA"] is not defined.', 1760942400); + } - $dataSet = include $filePath; $failMessages = []; - - foreach ($dataSet as $tableName => $expectedRecords) { + foreach ($this->getDataSet($filePath) as $tableName => $expectedRecords) { $records = $this->getAllRecords($tableName, (isset($GLOBALS['TCA'][$tableName]))); foreach ($expectedRecords as $assertion) { $result = $this->assertInRecords($assertion, $records); if ($result === false) { - // Handle error - if (isset($assertion['uid']) && empty($records[$assertion['uid']])) { - $failMessages[] = 'Record "' . $tableName . ':' . $assertion['uid'] . '" not found in database'; - continue; - } - if (isset($assertion['uid'])) { - $recordIdentifier = $tableName . ':' . $assertion['uid']; - $additionalInformation = $this->renderRecords($assertion, $records[$assertion['uid']]); - } else { - $recordIdentifier = $tableName; - $additionalInformation = $this->arrayToString($assertion); - } - - $failMessages[] = 'Assertion in data-set failed for "' . $recordIdentifier . '":' . PHP_EOL . $additionalInformation; + $failMessages[] = $this->getAssertionErrorMessageForNoneMatchingRecord($assertion, $records, $tableName); continue; } - // Unset asserted record + // Unset already asserted record to only keep unexpected records. unset($records[$result]); + // Increase assertion counter - self::assertTrue($result !== false); + self::assertTrue(true); } - if (!empty($records)) { - foreach ($records as $record) { - $recordIdentifier = $tableName . ':' . ($record['uid'] ?? ''); - $failMessages[] = 'Not asserted record found for "' . $recordIdentifier . '".'; + foreach ($records as $record) { + if (is_array($record) === false) { + throw new \RuntimeException('Something went horribly wrong while fetching records, record was not an array.', 1760943536); } + + $failMessages[] = $this->getAssertionErrorMessageForUnexpectedRecord($record, $tableName); } } + $failMessages = array_filter($failMessages); + if (!empty($failMessages)) { self::fail(implode(PHP_EOL, $failMessages)); } } + /** + * @return array[]> + */ + private function getDataSet(string $filePath): array + { + $this->ensureFileExists($filePath); + + $dataSet = require $filePath; + if (is_array($dataSet) === false) { + throw new \RuntimeException('Given file did not return an array: ' . $filePath, 1760942255); + } + + return $dataSet; + } + private function ensureFileExists(string $filePath): void { if (file_exists($filePath) === false) { throw new InvalidArgumentException('The requested PHP data-set file "' . $filePath . '" does not exist.', 1681207108); } } + + /** + * @param array{uid: int|string|null} $assertion + * @param array $records + */ + private function getAssertionErrorMessageForNoneMatchingRecord(array $assertion, array $records, string $tableName): string + { + // Handle error + if (isset($assertion['uid']) && empty($records[$assertion['uid']])) { + return 'Record "' . $tableName . ':' . $assertion['uid'] . '" not found in database'; + } + + if (isset($assertion['uid'])) { + $record = $records[$assertion['uid']] ?? null; + if (is_array($record) === false) { + return 'Assertion in data-set failed for "' . $tableName . ':' . $assertion['uid'] . '": Uid missing in database' . PHP_EOL; + } + + return 'Assertion in data-set failed for "' . $tableName . ':' . $assertion['uid'] . '":' . PHP_EOL . $this->renderRecords($assertion, $record); + } + + return 'Assertion in data-set failed for "' . $tableName . '":' . PHP_EOL . $this->arrayToString($assertion); + } + + /** + * @param mixed[] $record + */ + private function getAssertionErrorMessageForUnexpectedRecord(array $record, string $tableName): string + { + if (is_numeric($record['uid'] ?? null)) { + return sprintf( + 'Not asserted record with uid "%s" found for table "%s".', + $record['uid'], + $tableName + ); + } + + return sprintf( + 'Not asserted record found for table "%s": %s', + $tableName, + PHP_EOL . var_export($record, true) + ); + } } diff --git a/Tests/Functional/AssertTest.php b/Tests/Functional/AssertTest.php index 6cb515e..41c9298 100644 --- a/Tests/Functional/AssertTest.php +++ b/Tests/Functional/AssertTest.php @@ -120,7 +120,15 @@ public function failsForAssertionForMmRelation(): void ' \'sorting_foreign\' => \'3\'', ')', '', - 'Not asserted record found for "sys_category_record_mm:".', + 'Not asserted record found for table "sys_category_record_mm": ', + 'array (', + ' \'uid_local\' => 1,', + ' \'uid_foreign\' => 2,', + ' \'sorting\' => 0,', + ' \'sorting_foreign\' => 2,', + ' \'tablenames\' => \'pages\',', + ' \'fieldname\' => \'categories\',', + ')', ])); $this->assertPHPDataSet(__DIR__ . '/Fixtures/MmRelationBroken.php'); } @@ -131,7 +139,7 @@ public function failsForAdditionalNoneAssertedRecords(): void $this->importPHPDataSet(__DIR__ . '/Fixtures/WithDifferentColumns.php'); $this->expectException(AssertionFailedError::class); - $this->expectExceptionMessage('Not asserted record found for "pages:2".'); + $this->expectExceptionMessage('Not asserted record with uid "2" found for table "pages".'); $this->assertPHPDataSet(__DIR__ . '/Fixtures/AssertAdditionalRecords.php'); } diff --git a/Tests/Functional/Converter/CsvTest.php b/Tests/Functional/Converter/CsvTest.php index 989ad8c..515ca81 100644 --- a/Tests/Functional/Converter/CsvTest.php +++ b/Tests/Functional/Converter/CsvTest.php @@ -46,17 +46,6 @@ protected function tearDown(): void parent::tearDown(); } - #[Test] - public function canBeCreated(): void - { - $subject = new Csv(); - - self::assertInstanceOf( - Csv::class, - $subject - ); - } - #[Test] public function throwsExceptionForNoneExistingFile(): void { @@ -80,6 +69,9 @@ public function convertsCsvFileToPhpFile( self::assertFileEquals($expectedResultFile, $result); } + /** + * @return array + */ public static function possibleCsvFiles(): array { return [ diff --git a/Tests/Functional/Converter/XmlTest.php b/Tests/Functional/Converter/XmlTest.php index 432aba8..8552870 100644 --- a/Tests/Functional/Converter/XmlTest.php +++ b/Tests/Functional/Converter/XmlTest.php @@ -46,17 +46,6 @@ protected function tearDown(): void parent::tearDown(); } - #[Test] - public function canBeCreated(): void - { - $subject = new Xml(); - - self::assertInstanceOf( - Xml::class, - $subject - ); - } - #[Test] public function throwsExceptionForNoneExistingFile(): void { @@ -80,6 +69,9 @@ public function convertsXmlFileToPhpFile( self::assertFileEquals($expectedResultFile, $result); } + /** + * @return array + */ public static function possibleXmlFiles(): array { return [ diff --git a/Tests/Functional/ImportTest.php b/Tests/Functional/ImportTest.php index 09848f5..9d446cb 100644 --- a/Tests/Functional/ImportTest.php +++ b/Tests/Functional/ImportTest.php @@ -54,6 +54,7 @@ public function canImportWithNullValue(): void $this->importPHPDataSet(__DIR__ . '/Fixtures/WithNull.php'); $records = $this->getAllRecords('pages', true); + self::assertIsArray($records[1]); self::assertNull($records[1]['description']); } diff --git a/composer.json b/composer.json index c11e23c..975276e 100644 --- a/composer.json +++ b/composer.json @@ -35,9 +35,12 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.4", - "phpstan/phpstan": "^1.10", - "phpstan/phpstan-phpunit": "^1.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^11.5 || ^12.2", + "tomasvotruba/cognitive-complexity": "^1.0", "typo3/testing-framework": "^9.2" }, "extra": { @@ -48,6 +51,7 @@ }, "config": { "allow-plugins": { + "phpstan/extension-installer": true, "typo3/class-alias-loader": true, "typo3/cms-composer-installers": true }, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 30dd237..04fbd11 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,6 +1,49 @@ parameters: ignoreErrors: - - message: "#^Method Codappix\\\\Typo3PhpDatasets\\\\PhpDataSet\\:\\:getConnectionPool\\(\\) should return TYPO3\\\\CMS\\\\Core\\\\Database\\\\ConnectionPool but returns object\\.$#" + rawMessage: 'Cognitive complexity for "Codappix\Typo3PhpDatasets\Converter\Csv::buildContent()" is 11, keep it under 9' + identifier: complexity.functionLike count: 1 - path: Classes/PhpDataSet.php + path: Classes/Converter/Csv.php + + - + rawMessage: 'Parameter #1 $keys of function array_combine expects array, array given.' + identifier: argument.type + count: 1 + path: Classes/Converter/Csv.php + + - + rawMessage: Cannot cast mixed to string. + identifier: cast.string + count: 1 + path: Classes/Converter/Xml.php + + - + rawMessage: 'Call to static method PHPUnit\Framework\Assert::assertTrue() with true will always evaluate to true.' + identifier: staticMethod.alreadyNarrowedType + count: 1 + path: Tests/Functional/AbstractFunctionalTestCase.php + + - + rawMessage: 'Cognitive complexity for "Codappix\Typo3PhpDatasets\Tests\Functional\AbstractFunctionalTestCase::assertPHPDataSet()" is 12, keep it under 9' + identifier: complexity.functionLike + count: 1 + path: Tests/Functional/AbstractFunctionalTestCase.php + + - + rawMessage: 'Method Codappix\Typo3PhpDatasets\Tests\Functional\AbstractFunctionalTestCase::getDataSet() should return array>> but returns array.' + identifier: return.type + count: 1 + path: Tests/Functional/AbstractFunctionalTestCase.php + + - + rawMessage: 'Parameter #1 $assertion of method Codappix\Typo3PhpDatasets\Tests\Functional\AbstractFunctionalTestCase::getAssertionErrorMessageForNoneMatchingRecord() expects array{uid: int|string|null}, array given.' + identifier: argument.type + count: 1 + path: Tests/Functional/AbstractFunctionalTestCase.php + + - + rawMessage: 'Parameter #2 $records of method Codappix\Typo3PhpDatasets\Tests\Functional\AbstractFunctionalTestCase::getAssertionErrorMessageForNoneMatchingRecord() expects array>, array given.' + identifier: argument.type + count: 1 + path: Tests/Functional/AbstractFunctionalTestCase.php diff --git a/phpstan.neon b/phpstan.neon index 88cf47b..2f5503f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,10 +1,17 @@ includes: - - phpstan-baseline.neon + - 'phpstan-baseline.neon' + - 'phar://phpstan.phar/conf/bleedingEdge.neon' parameters: - level: max + level: 'max' paths: - - Classes - - Tests - checkMissingIterableValueType: false - reportUnmatchedIgnoredErrors: false - checkGenericClassInNonGenericObjectType: false + - 'Classes' + - 'Tests' + cognitive_complexity: + class: 40 + function: 9 + ignoreErrors: + # We explicitly want to test the PHPUnit integration + - + rawMessage: 'Access to constant on internal class PHPUnit\Framework\AssertionFailedError.' + identifier: 'classConstant.internalClass' + path: 'Tests/Functional/' From c2a322502e43609df7d8b383975b22eca01ebb25 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Mon, 20 Oct 2025 10:48:23 +0200 Subject: [PATCH 4/4] Improve PHPStan situation --- Classes/TestingFramework.php | 8 ++++---- phpstan-baseline.neon | 24 ------------------------ 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/Classes/TestingFramework.php b/Classes/TestingFramework.php index 60b1823..01a5cdb 100644 --- a/Classes/TestingFramework.php +++ b/Classes/TestingFramework.php @@ -68,7 +68,7 @@ protected function assertPHPDataSet(string $filePath): void // Unset already asserted record to only keep unexpected records. unset($records[$result]); - // Increase assertion counter + // @phpstan-ignore staticMethod.alreadyNarrowedType (We want to increase assertion counter, but there is no public API) self::assertTrue(true); } @@ -100,6 +100,7 @@ private function getDataSet(string $filePath): array throw new \RuntimeException('Given file did not return an array: ' . $filePath, 1760942255); } + // @phpstan-ignore return.type (We don't want to validate the whole structure) return $dataSet; } @@ -111,12 +112,11 @@ private function ensureFileExists(string $filePath): void } /** - * @param array{uid: int|string|null} $assertion - * @param array $records + * @param array $assertion + * @param mixed[] $records */ private function getAssertionErrorMessageForNoneMatchingRecord(array $assertion, array $records, string $tableName): string { - // Handle error if (isset($assertion['uid']) && empty($records[$assertion['uid']])) { return 'Record "' . $tableName . ':' . $assertion['uid'] . '" not found in database'; } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 04fbd11..da1396d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -18,32 +18,8 @@ parameters: count: 1 path: Classes/Converter/Xml.php - - - rawMessage: 'Call to static method PHPUnit\Framework\Assert::assertTrue() with true will always evaluate to true.' - identifier: staticMethod.alreadyNarrowedType - count: 1 - path: Tests/Functional/AbstractFunctionalTestCase.php - - rawMessage: 'Cognitive complexity for "Codappix\Typo3PhpDatasets\Tests\Functional\AbstractFunctionalTestCase::assertPHPDataSet()" is 12, keep it under 9' identifier: complexity.functionLike count: 1 path: Tests/Functional/AbstractFunctionalTestCase.php - - - - rawMessage: 'Method Codappix\Typo3PhpDatasets\Tests\Functional\AbstractFunctionalTestCase::getDataSet() should return array>> but returns array.' - identifier: return.type - count: 1 - path: Tests/Functional/AbstractFunctionalTestCase.php - - - - rawMessage: 'Parameter #1 $assertion of method Codappix\Typo3PhpDatasets\Tests\Functional\AbstractFunctionalTestCase::getAssertionErrorMessageForNoneMatchingRecord() expects array{uid: int|string|null}, array given.' - identifier: argument.type - count: 1 - path: Tests/Functional/AbstractFunctionalTestCase.php - - - - rawMessage: 'Parameter #2 $records of method Codappix\Typo3PhpDatasets\Tests\Functional\AbstractFunctionalTestCase::getAssertionErrorMessageForNoneMatchingRecord() expects array>, array given.' - identifier: argument.type - count: 1 - path: Tests/Functional/AbstractFunctionalTestCase.php