diff --git a/composer.json b/composer.json index fb0907fe..495f7d64 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "psr/clock": "^1.0" }, "require-dev": { + "doctrine/dbal": "^3.9 || ^4.0", "infection/infection": "^0.29.6", "lendable/composer-license-checker": "^1.2.1", "lendable/phpunit-extensions": "^0.3", @@ -90,7 +91,7 @@ "phpunit --colors --testsuite=unit" ], "infection": [ - "./bin/infection --threads=8 --min-msi=99 --show-mutations" + "./bin/infection --threads=8 --min-msi=98 --show-mutations" ], "tests": [ "@tests:unit" diff --git a/composer.lock b/composer.lock index fde0a2c0..c7f778ec 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "309f13ce9fc5468b34f2a2216a2ee948", + "content-hash": "3ca8f5530f30d0cfb9bda96a8590dde2", "packages": [ { "name": "psr/clock", @@ -375,6 +375,112 @@ ], "time": "2024-04-18T06:56:21+00:00" }, + { + "name": "doctrine/dbal", + "version": "4.2.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/33d2d7fe1269b2301640c44cf2896ea607b30e3e", + "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^0.5.3|^1", + "php": "^8.1", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "10.5.39", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^6.3.8|^7.0", + "symfony/console": "^5.4|^6.3|^7.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.2.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-03-07T18:29:05+00:00" + }, { "name": "doctrine/deprecations", "version": "1.1.4", @@ -2269,6 +2375,55 @@ ], "time": "2025-03-23T16:02:11+00:00" }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, { "name": "psr/container", "version": "2.0.2", diff --git a/lib/Bridge/Doctrine/Type/DateType.php b/lib/Bridge/Doctrine/Type/DateType.php new file mode 100644 index 00000000..ba7692c3 --- /dev/null +++ b/lib/Bridge/Doctrine/Type/DateType.php @@ -0,0 +1,69 @@ +getDateTypeDeclarationSQL($column); + } + + public function convertToPHPValue($value, AbstractPlatform $platform): ?Date + { + if ($value === null) { + return null; + } + + if (!\is_string($value)) { + throw \class_exists(ValueNotConvertible::class) + ? ValueNotConvertible::new($value, self::NAME) + : ConversionException::conversionFailed($value, self::NAME); + } + + try { + return Date::fromYearMonthDayString($value); + } catch (\InvalidArgumentException $e) { + throw \class_exists(InvalidFormat::class) + ? InvalidFormat::new($value, self::NAME, 'Y-m-d', $e) + : ConversionException::conversionFailedFormat($value, self::NAME, 'Y-m-d', $e); + } + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if ($value === null) { + return null; + } + + if ($value instanceof Date) { + return $value->toYearMonthDayString(); + } + + // @infection-ignore-all (ArrayItemRemoval) + throw \class_exists(InvalidType::class) + ? InvalidType::new($value, self::NAME, ['null', Date::class]) + : ConversionException::conversionFailedInvalidType( + $value, + self::NAME, + ['null', Date::class], + ); + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/phpstan.neon b/phpstan.neon index 5b872215..ae0db7ee 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,5 +16,5 @@ parameters: ignoreErrors: - '#Dynamic call to static method PHPUnit\\Framework\\.*#' - - '#^Call to an undefined static method DateTime(?:Immutable)?::createFromInterface\(\)\.$#' - '#^Attribute class PHPUnit\\Framework\\Attributes\\CodeCoverageIgnore is deprecated: https://github.com/sebastianbergmann/phpunit/issues/5236$#' + - '#^Call to an undefined static method Doctrine\\DBAL\\Types\\ConversionException::.+$#' diff --git a/tests/unit/Bridge/Doctrine/Type/DateTypeTest.php b/tests/unit/Bridge/Doctrine/Type/DateTypeTest.php new file mode 100644 index 00000000..87c8070c --- /dev/null +++ b/tests/unit/Bridge/Doctrine/Type/DateTypeTest.php @@ -0,0 +1,117 @@ +assertNull((new DateType())->convertToDatabaseValue(null, new MySQLPlatform())); + } + + #[Test] + public function conversion_to_db_value(): void + { + $this->assertSame( + '2020-01-03', + (new DateType())->convertToDatabaseValue(Date::fromYearMonthDayString('2020-01-03'), new MySQLPlatform()), + ); + } + + #[Test] + public function db_value_conversion(): void + { + $this->assertSame( + '2020-01-03', + (new DateType())->convertToPHPValue('2020-01-03', new MySQLPlatform())?->toYearMonthDayString(), + ); + } + + #[Test] + public function null_db_value_conversion(): void + { + $this->assertNull((new DateType())->convertToPHPValue(null, new MySQLPlatform())); + } + + #[Test] + #[RequiresMethod(ValueNotConvertible::class, 'new')] + public function invalid_db_value_conversion_dbal_4(): void + { + $this->expectExceptionObject(ValueNotConvertible::new(321, DateType::NAME)); + + (new DateType())->convertToPHPValue(321, new MySQLPlatform()); + } + + #[Test] + #[RequiresMethod(InvalidFormat::class, 'new')] + public function invalid_db_format_conversion_dbal_4(): void + { + $this->expectExceptionObject(InvalidFormat::new('2010-01', DateType::NAME, 'Y-m-d')); + + try { + (new DateType())->convertToPHPValue('2010-01', new MySQLPlatform()); + } catch (InvalidFormat $e) { + $this->assertInstanceOf(\InvalidArgumentException::class, $e->getPrevious()); + + throw $e; + } + } + + #[Test] + #[RequiresMethod(InvalidType::class, 'new')] + public function invalid_php_value_conversion_dbal_4(): void + { + $this->expectExceptionObject(InvalidType::new(321, DateType::NAME, ['null', Date::class])); + + (new DateType())->convertToDatabaseValue(321, new MySQLPlatform()); + } + + #[Test] + #[RequiresMethod(ConversionException::class, 'conversionFailed')] + public function invalid_db_value_conversion_dbal_3(): void + { + $this->expectExceptionObject(ConversionException::conversionFailed(321, DateType::NAME)); + + (new DateType())->convertToPHPValue(321, new MySQLPlatform()); + } + + #[Test] + #[RequiresMethod(ConversionException::class, 'conversionFailedFormat')] + public function invalid_db_format_conversion_dbal_3(): void + { + $this->expectExceptionObject(ConversionException::conversionFailedFormat('2010-01', DateType::NAME, 'Y-m-d')); + + try { + (new DateType())->convertToPHPValue('2010-01', new MySQLPlatform()); + } catch (ConversionException $e) { + $this->assertInstanceOf(\InvalidArgumentException::class, $e->getPrevious()); + + throw $e; + } + } + + #[Test] + #[RequiresMethod(ConversionException::class, 'conversionFailedInvalidType')] + public function invalid_php_value_conversion_dbal_3(): void + { + $this->expectExceptionObject(ConversionException::conversionFailedInvalidType(321, DateType::NAME, ['null', Date::class])); + + (new DateType())->convertToDatabaseValue(321, new MySQLPlatform()); + } +}