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/.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..a057814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 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. + 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 + +- 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/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 new file mode 100644 index 0000000..eef726c --- /dev/null +++ b/Classes/Connection.php @@ -0,0 +1,61 @@ + + * + * 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 TYPO3\CMS\Core\Database\Connection as Typo3Connection; + +/** + * @internal + */ +final readonly class Connection +{ + public function __construct( + private Typo3Connection $typo3Connection, + ) { + } + + /** + * @param string[] $record + * @param ParameterType[] $types + */ + 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( + $tableName, + $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/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 a8ad5cb..11903b2 100644 --- a/Classes/PhpDataSet.php +++ b/Classes/PhpDataSet.php @@ -23,40 +23,27 @@ namespace Codappix\Typo3PhpDatasets; -use RuntimeException; -use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Utility\GeneralUtility; - +/** + * @api + */ class PhpDataSet { + /** + * @api + * @param array[]> $dataSet + */ 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..0fc7b7e --- /dev/null +++ b/Classes/Table.php @@ -0,0 +1,57 @@ + + * + * 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 string $tableName, + 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->tableName, + ), 1760941225); + } + } +} diff --git a/Classes/TestingFramework.php b/Classes/TestingFramework.php index fc81b83..01a5cdb 100644 --- a/Classes/TestingFramework.php +++ b/Classes/TestingFramework.php @@ -27,15 +27,16 @@ 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); - - $dataSet = include $filePath; + $dataSet = $this->getDataSet($filePath); try { (new PhpDataSet())->import($dataSet); } catch (Exception $e) { @@ -45,60 +46,110 @@ protected function importPHPDataSet(string $filePath): void /** * Highly inspired by TYPO3 testing framework. + * @api */ 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); + + // @phpstan-ignore staticMethod.alreadyNarrowedType (We want to increase assertion counter, but there is no public API) + 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); + } + + // @phpstan-ignore return.type (We don't want to validate the whole structure) + 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 $assertion + * @param mixed[] $records + */ + private function getAssertionErrorMessageForNoneMatchingRecord(array $assertion, array $records, string $tableName): string + { + 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 52a461d..41c9298 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'); @@ -133,26 +120,30 @@ 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'); } - /** - * @test - */ + #[Test] 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'); } - /** - * @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..515ca81 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,22 +46,7 @@ protected function tearDown(): void parent::tearDown(); } - /** - * @test - */ - public function canBeCreated(): void - { - $subject = new Csv(); - - self::assertInstanceOf( - Csv::class, - $subject - ); - } - - /** - * @test - */ + #[Test] public function throwsExceptionForNoneExistingFile(): void { $subject = new Csv(); @@ -69,11 +56,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 @@ -84,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 769ef1f..8552870 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,22 +46,7 @@ protected function tearDown(): void parent::tearDown(); } - /** - * @test - */ - public function canBeCreated(): void - { - $subject = new Xml(); - - self::assertInstanceOf( - Xml::class, - $subject - ); - } - - /** - * @test - */ + #[Test] public function throwsExceptionForNoneExistingFile(): void { $subject = new Xml(); @@ -69,11 +56,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 @@ -84,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 3a4b8fc..9d446cb 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,20 +48,17 @@ public function canImportSimpleSet(): void self::assertSame('Some text', $records[1]['description']); } - /** - * @test - */ + #[Test] public function canImportWithNullValue(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/WithNull.php'); $records = $this->getAllRecords('pages', true); + self::assertIsArray($records[1]); self::assertNull($records[1]['description']); } - /** - * @test - */ + #[Test] public function canImportRecordsWithDifferentSetOfColumns(): void { $this->importPHPDataSet(__DIR__ . '/Fixtures/WithDifferentColumns.php'); @@ -73,23 +71,19 @@ public function canImportRecordsWithDifferentSetOfColumns(): void self::assertSame('Some other text', $records[2]['description']); } - /** - * @test - */ + #[Test] public function failsIfSqlError(): void { $this->expectException(AssertionFailedError::class); $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'); } - /** - * @test - */ + #[Test] public function throwsExceptionIfFileDoesNotExist(): void { $this->expectException(InvalidArgumentException::class); diff --git a/composer.json b/composer.json index 39bfcc0..975276e 100644 --- a/composer.json +++ b/composer.json @@ -27,18 +27,21 @@ "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" + "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": { "typo3/cms": { @@ -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..da1396d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,6 +1,25 @@ 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: '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 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/'