From 64d4bd89e2d08cb01609afef30b677f4a3de9618 Mon Sep 17 00:00:00 2001 From: Courtney Miles Date: Mon, 30 Oct 2023 07:28:55 +1000 Subject: [PATCH] PHP 8.1 compatibility (#72) * PHP 8.1 compatibility * update github action to run tests for php 8.1 * Force the version of CS Fixer * Update PHPUnit before running tests --- .github/workflows/tests.yml | 6 +- .install-cs-fixer.sh | 2 +- Dockerfile | 6 +- composer.json | 2 +- src/Fields/BaseField.php | 3 + src/Fields/DateField.php | 7 +- src/Fields/DatetimeField.php | 9 +- src/Fields/TimeField.php | 11 +- src/Table.php | 5 + src/Utility/StrptimeFormatTransformer.php | 53 ++++++ tests/FieldTest.php | 20 +- tests/Fields/DateFieldTest.php | 38 ++++ tests/Fields/DatetimeFieldTest.php | 38 ++++ tests/Fields/TimeFieldTest.php | 33 ++++ tests/SchemaTest.php | 44 ++--- tests/TableTest.php | 3 +- .../StrptimeFormatTransformerTestCase.php | 178 ++++++++++++++++++ 17 files changed, 408 insertions(+), 50 deletions(-) create mode 100644 src/Utility/StrptimeFormatTransformer.php create mode 100644 tests/Fields/DateFieldTest.php create mode 100644 tests/Fields/DatetimeFieldTest.php create mode 100644 tests/Fields/TimeFieldTest.php create mode 100644 tests/Utility/StrptimeFormatTransformerTestCase.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cacbe96..90fbe93 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: ['7.1', '7.2', '7.3', '7.4', '8.0'] + php-version: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] composer-prefer: - '--prefer-dist' - '--prefer-stable --prefer-lowest' @@ -39,7 +39,9 @@ jobs: restore-keys: | ${{ runner.os }}-php- - name: Install dependencies - run: composer update ${{ matrix.composer-prefer }} --no-progress + run: | + composer update ${{ matrix.composer-prefer }} --no-progress + composer update phpunit/phpunit --no-progress - name: Run Code Style Check for PHP ${{ matrix.php-version }} run: composer run-script style-check diff --git a/.install-cs-fixer.sh b/.install-cs-fixer.sh index e8e5a20..310ab2e 100755 --- a/.install-cs-fixer.sh +++ b/.install-cs-fixer.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash if [ ! -f ./php-cs-fixer ]; then - wget https://cs.symfony.com/download/php-cs-fixer-v3.phar -O php-cs-fixer + wget https://cs.symfony.com/download/v3.4.0/php-cs-fixer.phar -O php-cs-fixer chmod +x php-cs-fixer fi diff --git a/Dockerfile b/Dockerfile index 9644422..f707c89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -FROM php:8.0-cli +FROM php:8.1-cli -RUN apt-get update +RUN apt-get update -y ## PHP dependencies RUN pecl install xdebug \ @@ -11,4 +11,4 @@ RUN curl -sS https://getcomposer.org/installer | php \ && apt-get install git unzip -y ENV COMPOSER_ALLOW_SUPERUSER=1 ENV XDEBUG_MODE=coverage -WORKDIR /src \ No newline at end of file +WORKDIR /src diff --git a/composer.json b/composer.json index 606b3e7..6c44f5c 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "jmikola/geojson": "^1.0" }, "require-dev": { - "phpunit/phpunit": ">=7.5", + "phpunit/phpunit": ">=7.5 <10.0", "php-coveralls/php-coveralls": "^2.4", "psy/psysh": "@stable", "roave/security-advisories": "dev-latest" diff --git a/src/Fields/BaseField.php b/src/Fields/BaseField.php index 9ddc3f4..d3074b5 100644 --- a/src/Fields/BaseField.php +++ b/src/Fields/BaseField.php @@ -7,6 +7,9 @@ abstract class BaseField { + /** + * @param object $descriptor + */ public function __construct($descriptor = null) { $this->descriptor = empty($descriptor) ? (object) [] : $descriptor; diff --git a/src/Fields/DateField.php b/src/Fields/DateField.php index 77ea2e2..6e1be82 100644 --- a/src/Fields/DateField.php +++ b/src/Fields/DateField.php @@ -3,6 +3,7 @@ namespace frictionlessdata\tableschema\Fields; use Carbon\Carbon; +use frictionlessdata\tableschema\Utility\StrptimeFormatTransformer; class DateField extends BaseField { @@ -21,13 +22,13 @@ protected function validateCastValue($val) } } else { $format = 'default' === $this->format() ? self::DEFAULT_FORMAT : $this->format(); - $date = strptime($val, $format); + $date = date_parse_from_format(StrptimeFormatTransformer::transform($format), $val); - if (false === $date || '' != $date['unparsed']) { + if ($date['error_count'] > 0) { throw $this->getValidationException("couldn't parse date/time according to given strptime format '{$format}''", $val); } else { return Carbon::create( - (int) $date['tm_year'] + 1900, (int) $date['tm_mon'] + 1, (int) $date['tm_mday'], + (int) $date['year'], (int) $date['month'], (int) $date['day'], 0, 0, 0 ); } diff --git a/src/Fields/DatetimeField.php b/src/Fields/DatetimeField.php index 778cc20..5b10bda 100644 --- a/src/Fields/DatetimeField.php +++ b/src/Fields/DatetimeField.php @@ -3,6 +3,7 @@ namespace frictionlessdata\tableschema\Fields; use Carbon\Carbon; +use frictionlessdata\tableschema\Utility\StrptimeFormatTransformer; class DatetimeField extends BaseField { @@ -33,13 +34,13 @@ protected function validateCastValue($val) throw $this->getValidationException($e->getMessage(), $val); } default: - $date = strptime($val, $this->format()); - if (false === $date || '' != $date['unparsed']) { + $date = date_parse_from_format(StrptimeFormatTransformer::transform($this->format()), $val); + if ($date['error_count'] > 0) { throw $this->getValidationException("couldn't parse date/time according to given strptime format '{$this->format()}''", $val); } else { return Carbon::create( - (int) $date['tm_year'] + 1900, (int) $date['tm_mon'] + 1, (int) $date['tm_mday'], - (int) $date['tm_hour'], (int) $date['tm_min'], (int) $date['tm_sec'] + (int) $date['year'], (int) $date['month'], (int) $date['day'], + (int) $date['hour'], (int) $date['minute'], (int) $date['second'] ); } } diff --git a/src/Fields/TimeField.php b/src/Fields/TimeField.php index 7829f5b..9a50578 100644 --- a/src/Fields/TimeField.php +++ b/src/Fields/TimeField.php @@ -3,6 +3,7 @@ namespace frictionlessdata\tableschema\Fields; use Carbon\Carbon; +use frictionlessdata\tableschema\Utility\StrptimeFormatTransformer; /** * Class TimeField @@ -32,11 +33,15 @@ protected function validateCastValue($val) return $this->getNativeTime($dt->hour, $dt->minute, $dt->second); default: - $date = strptime($val, $this->format()); - if (false === $date || '' != $date['unparsed']) { + $date = date_parse_from_format( + StrptimeFormatTransformer::transform($this->format()), + $val + ); + + if ($date['error_count'] > 0) { throw $this->getValidationException(null, $val); } else { - return $this->getNativeTime($date['tm_hour'], $date['tm_min'], $date['tm_sec']); + return $this->getNativeTime($date['hour'], $date['minute'], $date['second']); } } } diff --git a/src/Table.php b/src/Table.php index a1c66c2..265dbbf 100644 --- a/src/Table.php +++ b/src/Table.php @@ -160,6 +160,7 @@ public function save($outputDataSource) * @throws Exceptions\FieldValidationException * @throws Exceptions\DataSourceException */ + #[\ReturnTypeWillChange] public function current() { if (count($this->castRows) > 0) { @@ -213,6 +214,7 @@ public function __destruct() $this->dataSource->close(); } + #[\ReturnTypeWillChange] public function rewind() { if (0 == $this->currentLine) { @@ -223,11 +225,13 @@ public function rewind() } } + #[\ReturnTypeWillChange] public function key() { return $this->currentLine - count($this->castRows); } + #[\ReturnTypeWillChange] public function next() { if (0 == count($this->castRows)) { @@ -235,6 +239,7 @@ public function next() } } + #[\ReturnTypeWillChange] public function valid() { return count($this->castRows) > 0 || !$this->dataSource->isEof(); diff --git a/src/Utility/StrptimeFormatTransformer.php b/src/Utility/StrptimeFormatTransformer.php new file mode 100644 index 0000000..441cb3c --- /dev/null +++ b/src/Utility/StrptimeFormatTransformer.php @@ -0,0 +1,53 @@ + 'd', // 09 + '%e' => 'j', // 9 + + // Month + '%m' => 'm', // 02 + '%b' => 'M', // Feb + '%B' => 'F', // February + + // Year + '%Y' => 'Y', // 2023 + '%y' => 'y', // 23 + + // Hour + '%H' => 'H', // 00 to 23 + '%k' => 'G', // 0 to 23 + '%I' => 'h', // 00 to 12 + '%l' => 'h', // 0 to 12 + '%p' => 'A', // AM / PM + '%P' => 'a', // am / pm + + // Minute + '%M' => 'i', + + // Second + '%S' => 's', + + // Date + '%D' => 'm/d/y', + '%F' => 'Y-m-d', + + // Time + '%r' => 'h:i:s A', + '%R' => 'H:i', + '%T' => 'H:i:s', + '%s' => 'U', + ] + ); + } +} diff --git a/tests/FieldTest.php b/tests/FieldTest.php index 1b656a6..29cb120 100644 --- a/tests/FieldTest.php +++ b/tests/FieldTest.php @@ -79,7 +79,7 @@ public function testType(string $expectedType, array $fieldDescriptor): void ); } - public function provideFieldWithType(): array + public static function provideFieldWithType(): array { return [ [ @@ -104,7 +104,7 @@ public function testFormat(string $expectedFormat, array $fieldDescriptor): void ); } - public function provideFieldDescriptorFormat(): array + public static function provideFieldDescriptorFormat(): array { return [ [ @@ -129,7 +129,7 @@ public function testConstraints(\stdClass $expectedConstraint, array $fieldDescr ); } - public function provideFieldConstraintsTestData(): array + public static function provideFieldConstraintsTestData(): array { return [ [ @@ -158,7 +158,7 @@ public function testRequired(bool $expectedRequired, array $fieldDescriptor): vo ); } - public function provideFieldRequiredTestData(): array + public static function provideFieldRequiredTestData(): array { return [ [ @@ -231,7 +231,7 @@ public function testDisableConstraints($expectedCastValue, $valueToCast, array $ ); } - public function provideDisableConstraintTestData(): array + public static function provideDisableConstraintTestData(): array { return [ [ @@ -285,7 +285,7 @@ public function testValidateValue(string $expectedError, array $fieldDescriptor, $this->assertFieldValidateValue($expectedError, $fieldDescriptor, $value); } - public function provideValidateValueTestData(): array + public static function provideValidateValueTestData(): array { return [ [ @@ -321,7 +321,7 @@ public function testValidateValueDisableConstraints(array $fieldDescriptor, $val ); } - public function provideValidateValueDisableConstraintsTestData(): array + public static function provideValidateValueDisableConstraintsTestData(): array { return [ [ @@ -347,7 +347,7 @@ public function testMissingValues(string $fieldType): void $this->assertMissingValues(['type' => $fieldType], ['', 'NA', 'N/A']); } - public function provideMissingDataFieldType(): array + public static function provideMissingDataFieldType(): array { return [ ['string'], @@ -427,7 +427,7 @@ public function testValidValueForConstraint(string $type, array $constraintDefin $this->assertFieldValidateValue('', $descriptor, $validValue); } - public function provideValidDataForConstraint(): array + public static function provideValidDataForConstraint(): array { return [ ['string', ['pattern' => '3.*'], '3'], @@ -471,7 +471,7 @@ public function testInvalidValueForConstraint( $this->assertFieldValidateValue($expectedError, $descriptor, $invalidValue); } - public function provideInvalidDataForConstraint(): array + public static function provideInvalidDataForConstraint(): array { return [ ['name: value does not match pattern ("123")', 'string', ['pattern' => '3.*'], '123'], diff --git a/tests/Fields/DateFieldTest.php b/tests/Fields/DateFieldTest.php new file mode 100644 index 0000000..b45513c --- /dev/null +++ b/tests/Fields/DateFieldTest.php @@ -0,0 +1,38 @@ + $format]; + $sut = new DateField($descriptor); + + $castValue = $sut->castValue($dateValue); + self::assertInstanceOf(Carbon::class, $castValue); + + self::assertTrue( + $castValue->eq($expectedDateTime) + ); + } + + public static function provideDateFormatTestData(): iterable + { + yield [new DateTime('2023-02-28'), '%Y-%m-%d', '2023-02-28']; + } +} diff --git a/tests/Fields/DatetimeFieldTest.php b/tests/Fields/DatetimeFieldTest.php new file mode 100644 index 0000000..09899bf --- /dev/null +++ b/tests/Fields/DatetimeFieldTest.php @@ -0,0 +1,38 @@ + $format]; + $sut = new DatetimeField($descriptor); + + $castValue = $sut->castValue($dateValue); + self::assertInstanceOf(Carbon::class, $castValue); + + self::assertTrue( + $castValue->eq($expectedDateTime) + ); + } + + public static function provideDateFormatTestData(): iterable + { + yield [new DateTime('2023-02-28 13:12:34'), '%Y-%m-%d %H:%M:%S', '2023-02-28 13:12:34']; + } +} diff --git a/tests/Fields/TimeFieldTest.php b/tests/Fields/TimeFieldTest.php new file mode 100644 index 0000000..eb97012 --- /dev/null +++ b/tests/Fields/TimeFieldTest.php @@ -0,0 +1,33 @@ + $format]; + $sut = new TimeField($descriptor); + + self::assertSame( + $expectedTimeParts, + $sut->castValue($dateValue) + ); + } + + public static function provideDateFormatTestData(): iterable + { + yield [[13, 12, 34], '%H:%M:%S', '13:12:34']; + } +} diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 9d364da..30dc1e1 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -89,6 +89,17 @@ class SchemaTest extends TestCase private const FULL_DESCRIPTOR_FILE_PATH = __DIR__.DIRECTORY_SEPARATOR.'fixtures'.DIRECTORY_SEPARATOR.'schema_valid_full.json'; private const INVALID_DESCRIPTOR_FILE_PATH = __DIR__.DIRECTORY_SEPARATOR.'fixtures'.DIRECTORY_SEPARATOR.'schema_invalid_multiple_errors.json'; + protected static $tempFiles = []; + + public static function tearDownAfterClass(): void + { + foreach (self::$tempFiles as $tempFile) { + if (file_exists($tempFile)) { + unlink($tempFile); + } + } + } + public function testInitializeFromRemoteResource(): void { if (getenv('TABLESCHEMA_ENABLE_FRAGILE_TESTS')) { @@ -117,7 +128,7 @@ public function testConstructFromInvalidResource( new Schema($invalidSchema); } - public function provideInvalidSchema(): array + public static function provideInvalidSchema(): array { return [ [ @@ -145,7 +156,7 @@ public function testDifferentValidDescriptorSources(stdClass $expectedDescriptor $this->assertEquals($expectedDescriptor, $schema->descriptor()); } - public function provideValidDescriptorSources(): \Generator + public static function provideValidDescriptorSources(): \Generator { yield 'Simple object descriptor' => [ json_decode(self::SIMPLE_DESCRIPTOR_JSON, false), @@ -171,13 +182,13 @@ public function provideValidDescriptorSources(): \Generator json_decode(self::FULL_DESCRIPTOR_JSON, false), json_decode(self::FULL_DESCRIPTOR_JSON, true), ]; - $simpleDescriptorFilePath = $this->getTempFile(); + $simpleDescriptorFilePath = self::getTempFile(); file_put_contents($simpleDescriptorFilePath, self::SIMPLE_DESCRIPTOR_JSON); yield 'Simple JSON descriptor from file' => [ json_decode(self::SIMPLE_DESCRIPTOR_JSON, false), $simpleDescriptorFilePath, ]; - $fullDescriptorFilePath = $this->getTempFile(); + $fullDescriptorFilePath = self::getTempFile(); file_put_contents($fullDescriptorFilePath, self::FULL_DESCRIPTOR_JSON); yield 'Full JSON descriptor from file' => [ json_decode(self::FULL_DESCRIPTOR_JSON, false), @@ -195,7 +206,7 @@ public function testInvalidDescriptor(string $expectedErrors, $invalidDescriptor $this->assertValidationErrors($expectedErrors, $invalidDescriptor); } - public function provideInvalidDescriptors(): array + public static function provideInvalidDescriptors(): array { return [ [ @@ -289,7 +300,7 @@ public function testValidInitialize($validDescriptor): void self::assertTrue(true); } - public function provideValidDescriptors(): array + public static function provideValidDescriptors(): array { return [ [self::MIN_DESCRIPTOR_JSON], @@ -338,7 +349,7 @@ public function testCastRowNew(array $expectedRow, $descriptor, array $inputRow) $this->assertCastRow($expectedRow, $descriptor, $inputRow); } - public function provideRowCastingTestData(): \Generator + public static function provideRowCastingTestData(): \Generator { yield 'Cast integer field' => [ ['id' => 1, 'email' => 'test@example.com'], @@ -388,7 +399,7 @@ public function testCastException(string $expectedExceptionMessage, $descriptor, $schema->castRow($invalidRow); } - public function provideCastExceptionTestData(): array + public static function provideCastExceptionTestData(): array { return [ 'Wrong type in row' => [ @@ -432,7 +443,7 @@ public function testPrimaryKey(array $expectedKeys, $descriptor): void $this->assertEquals($expectedKeys, (new Schema($descriptor))->primaryKey()); } - public function providePrimaryKeyTestData(): \Generator + public static function providePrimaryKeyTestData(): \Generator { yield 'PK not defined' => [[], self::MIN_DESCRIPTOR_JSON]; yield 'PK defined as array' => [['id'], self::MAX_DESCRIPTOR_JSON]; @@ -608,17 +619,6 @@ public function testSchemaInferCsvDialect(): void ], $schema->descriptor()); } - public function tearDown(): void - { - foreach ($this->tempFiles as $tempFile) { - if (file_exists($tempFile)) { - unlink($tempFile); - } - } - } - - protected $tempFiles = []; - protected function assertValidationErrors($expectedValidationErrors, $descriptor): void { $this->assertEqualsIgnoringCase( @@ -648,10 +648,10 @@ protected function assertCastRowException($expectedError, $descriptor, $inputRow } } - protected function getTempFile() + protected static function getTempFile() { $file = tempnam(sys_get_temp_dir(), 'tableschema-php-tests'); - $this->tempFiles[] = $file; + self::$tempFiles[] = $file; return $file; } diff --git a/tests/TableTest.php b/tests/TableTest.php index 23703a8..23ec9ef 100644 --- a/tests/TableTest.php +++ b/tests/TableTest.php @@ -13,6 +13,7 @@ use frictionlessdata\tableschema\Schema; use frictionlessdata\tableschema\SchemaValidationError; use frictionlessdata\tableschema\Table; +use Generator; use PHPUnit\Framework\TestCase; class TableTest extends TestCase @@ -151,7 +152,7 @@ public function testEnforcePrimaryKey(string $expectedExceptionMessage, $descrip iterator_to_array($table); } - public function provideDuplicatePrimaryKeyTestData(): \Generator + public static function provideDuplicatePrimaryKeyTestData(): Generator { yield 'Null value not allowed for field in Primary Key' => [ 'row 1: value for id field cannot be null because it is part of the primary key', diff --git a/tests/Utility/StrptimeFormatTransformerTestCase.php b/tests/Utility/StrptimeFormatTransformerTestCase.php new file mode 100644 index 0000000..b4cc52b --- /dev/null +++ b/tests/Utility/StrptimeFormatTransformerTestCase.php @@ -0,0 +1,178 @@ + false, 'month' => false, 'day' => 3, 'hour' => false, 'minute' => false, 'second' => false], + '%d', + '03', + ]; + yield [ + ['year' => false, 'month' => false, 'day' => 3, 'hour' => false, 'minute' => false, 'second' => false], + '%e', + '3', + ]; + + // Month + yield [ + ['year' => false, 'month' => 2, 'day' => false, 'hour' => false, 'minute' => false, 'second' => false], + '%m', + '02', + ]; + yield [ + ['year' => false, 'month' => 2, 'day' => false, 'hour' => false, 'minute' => false, 'second' => false], + '%b', + 'Feb', + ]; + yield [ + ['year' => false, 'month' => 2, 'day' => false, 'hour' => false, 'minute' => false, 'second' => false], + '%B', + 'February', + ]; + + // Year + yield [ + ['year' => 2023, 'month' => false, 'day' => false, 'hour' => false, 'minute' => false, 'second' => false], + '%Y', + '2023', + ]; + yield [ + ['year' => 2023, 'month' => false, 'day' => false, 'hour' => false, 'minute' => false, 'second' => false], + '%y', + '23', + ]; + + // Hour + yield [ + ['year' => false, 'month' => false, 'day' => false, 'hour' => 9, 'minute' => 0, 'second' => 0], + '%H', + '09', + ]; + yield [ + ['year' => false, 'month' => false, 'day' => false, 'hour' => 9, 'minute' => 0, 'second' => 0], + '%k', + '9', + ]; + yield [ + ['year' => false, 'month' => false, 'day' => false, 'hour' => 23, 'minute' => 0, 'second' => 0], + '%I %p', + '11 PM', + ]; + yield [ + ['year' => false, 'month' => false, 'day' => false, 'hour' => 13, 'minute' => 0, 'second' => 0], + '%l %p', + '1 PM', + ]; + // Not understood by strptime +// yield [ +// ['year' => false, 'month' => false, 'day' => false, 'hour' => 13, 'minute' => 0, 'second' => 0], +// '%l %P', +// '1 pm', +// ]; + + // Minute + yield [ + ['year' => false, 'month' => false, 'day' => false, 'hour' => 0, 'minute' => 9, 'second' => 0], + '%M', + '09', + ]; + + // Second + yield [ + ['year' => false, 'month' => false, 'day' => false, 'hour' => 0, 'minute' => 0, 'second' => 9], + '%S', + '09', + ]; + + // Date + yield [ + ['year' => 2009, 'month' => 2, 'day' => 5, 'hour' => false, 'minute' => false, 'second' => false], + '%D', + '02/05/09', + ]; + yield [ + ['year' => 2009, 'month' => 2, 'day' => 5, 'hour' => false, 'minute' => false, 'second' => false], + '%F', + '2009-02-05', + ]; + + // Time + yield [ + ['year' => false, 'month' => false, 'day' => false, 'hour' => 21, 'minute' => 34, 'second' => 17], + '%r', + '09:34:17 PM', + ]; + yield [ + ['year' => false, 'month' => false, 'day' => false, 'hour' => 13, 'minute' => 23, 'second' => 0], + '%R', + '13:23', + ]; + yield [ + ['year' => false, 'month' => false, 'day' => false, 'hour' => 13, 'minute' => 23, 'second' => 34], + '%T', + '13:23:34', + ]; + yield [ + ['year' => 2023, 'month' => 10, 'day' => 27, 'hour' => 4, 'minute' => 32, 'second' => 44], + '%s', + '1698381164', + ]; + } + + private static function assertEquivalentStrptime(array $strptime, array $actual): void + { + if (0 === $strptime['tm_year']) { + self::assertFalse($actual['year']); + } else { + self::assertSame($strptime['tm_year'] + 1900, $actual['year']); + } + + if (0 === $strptime['tm_mon']) { + self::assertFalse($actual['month']); + } else { + self::assertSame($strptime['tm_mon'] + 1, $actual['month']); + } + + if (0 === $strptime['tm_mday']) { + self::assertFalse($actual['day']); + } else { + self::assertSame($strptime['tm_mday'], $actual['day']); + } + } +}