From ba7459486347c7d6d48fae366411e5234234d818 Mon Sep 17 00:00:00 2001 From: Marat Salakhov Date: Mon, 23 Oct 2023 14:31:16 +0300 Subject: [PATCH 1/8] Added dateTimeClass specification parameter (#182) --- src/DependencyInjection/Configuration.php | 1 + .../OpenApiServerExtension.php | 3 ++- src/Serializer/ArrayDtoSerializer.php | 7 ++++++- src/Specification/Definitions/Property.php | 13 ++++++++++++ .../Definitions/SpecificationConfig.php | 17 ++++++++++----- src/Specification/SpecificationLoader.php | 5 +++-- src/Specification/SpecificationParser.php | 14 +++++++++++-- src/Types/ScalarTypesResolver.php | 7 ++++++- src/Types/TypeSerializer.php | 21 +++++++++++++++---- 9 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 07a8b06c..c1062060 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -33,6 +33,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->isRequired() ->cannotBeEmpty() ->end() + ->scalarNode('date_time_class')->end() ->end() ->end() ->end(); diff --git a/src/DependencyInjection/OpenApiServerExtension.php b/src/DependencyInjection/OpenApiServerExtension.php index fea53b23..6a8d03ba 100644 --- a/src/DependencyInjection/OpenApiServerExtension.php +++ b/src/DependencyInjection/OpenApiServerExtension.php @@ -40,7 +40,8 @@ public function load(array $configs, ContainerBuilder $container): void * path: string, * type?: string, * name_space: string, - * media_type: string + * media_type: string, + * date_time_class?: string, * } * } $config */ diff --git a/src/Serializer/ArrayDtoSerializer.php b/src/Serializer/ArrayDtoSerializer.php index 9343b94a..93641898 100644 --- a/src/Serializer/ArrayDtoSerializer.php +++ b/src/Serializer/ArrayDtoSerializer.php @@ -113,8 +113,13 @@ private function convert(bool $deserialize, array $source, ObjectSchema $params) /** @psalm-suppress MissingClosureParamType */ $converter = fn ($v) => $this->convert($deserialize, $v, $objectType->getSchema()); } else { + $outputClass = null; + if ($typeId !== null && $this->resolver->isDateTime($typeId)) { + $outputClass = $property->getOutputType(); + } + /** @psalm-suppress MissingClosureParamType */ - $converter = fn ($v) => $this->resolver->convert($deserialize, $typeId ?? 0, $v); + $converter = fn ($v) => $this->resolver->convert($deserialize, $typeId ?? 0, $v, $outputClass); } if ($property->isArray()) { diff --git a/src/Specification/Definitions/Property.php b/src/Specification/Definitions/Property.php index a3469838..964d2cdb 100644 --- a/src/Specification/Definitions/Property.php +++ b/src/Specification/Definitions/Property.php @@ -17,6 +17,7 @@ final class Property private ObjectSchema|ObjectReference|null $objectTypeDefinition = null; private ?string $description = null; private ?string $pattern = null; + private ?object $outputType = null; public function __construct(string $name) { @@ -147,4 +148,16 @@ public function setNullable(bool $nullable): self return $this; } + + public function getOutputType(): ?object + { + return $this->outputType; + } + + public function setOutputType(?object $outputType): self + { + $this->outputType = $outputType; + + return $this; + } } diff --git a/src/Specification/Definitions/SpecificationConfig.php b/src/Specification/Definitions/SpecificationConfig.php index b984fb99..0a322297 100644 --- a/src/Specification/Definitions/SpecificationConfig.php +++ b/src/Specification/Definitions/SpecificationConfig.php @@ -10,13 +10,15 @@ final class SpecificationConfig private ?string $type; private string $nameSpace; private string $mediaType; + private ?string $dateTimeClass; - public function __construct(string $path, ?string $type, string $nameSpace, string $mediaType) + public function __construct(string $path, ?string $type, string $nameSpace, string $mediaType, ?string $dateTimeClass = null) { - $this->path = $path; - $this->type = $type; - $this->nameSpace = $nameSpace; - $this->mediaType = $mediaType; + $this->path = $path; + $this->type = $type; + $this->nameSpace = $nameSpace; + $this->mediaType = $mediaType; + $this->dateTimeClass = $dateTimeClass; } public function getPath(): string @@ -38,4 +40,9 @@ public function getMediaType(): string { return $this->mediaType; } + + public function getDateTimeClass(): ?string + { + return $this->dateTimeClass; + } } diff --git a/src/Specification/SpecificationLoader.php b/src/Specification/SpecificationLoader.php index dc034866..704246fa 100644 --- a/src/Specification/SpecificationLoader.php +++ b/src/Specification/SpecificationLoader.php @@ -45,7 +45,7 @@ public function __construct(SpecificationParser $parser, FileLocatorInterface $l } /** - * @param array{path:string,type:string|null,name_space:string,media_type:string} $spec + * @param array{path:string,type:string|null,name_space:string,media_type:string,date_time_class:string|null} $spec */ public function registerSpec(string $name, array $spec): void { @@ -53,7 +53,8 @@ public function registerSpec(string $name, array $spec): void $spec['path'], $spec['type'] ?? null, $spec['name_space'], - $spec['media_type'] + $spec['media_type'], + $spec['date_time_class'] ?? null ); } diff --git a/src/Specification/SpecificationParser.php b/src/Specification/SpecificationParser.php index 7612e350..b6154139 100644 --- a/src/Specification/SpecificationParser.php +++ b/src/Specification/SpecificationParser.php @@ -15,6 +15,7 @@ use cebe\openapi\spec\Responses; use cebe\openapi\spec\Schema; use cebe\openapi\spec\Type; +use DateTimeInterface; use OnMoon\OpenApiServerBundle\Exception\CannotParseOpenApi; use OnMoon\OpenApiServerBundle\Specification\Definitions\ComponentArray; use OnMoon\OpenApiServerBundle\Specification\Definitions\ObjectReference; @@ -44,12 +45,12 @@ class SpecificationParser private ScalarTypesResolver $typeResolver; /** @var string[] */ private array $skipHttpCodes; + private ?DateTimeInterface $dateTimeClass = null; /** @param array $skipHttpCodes */ public function __construct(ScalarTypesResolver $typeResolver, array $skipHttpCodes) { - $this->typeResolver = $typeResolver; - + $this->typeResolver = $typeResolver; $this->skipHttpCodes = array_map(static fn ($code) => (string) $code, $skipHttpCodes); } @@ -58,6 +59,11 @@ public function parseOpenApi(string $specificationName, SpecificationConfig $spe $componentSchemas = new ComponentArray(); $operationDefinitions = []; + + if ($specificationConfig->getDateTimeClass() !== null) { + $this->dateTimeClass = new ($specificationConfig->getDateTimeClass())(); + } + /** * @var string $url */ @@ -385,6 +391,10 @@ private function getProperty( if (Type::isScalar($itemProperty->type)) { $scalarTypeId = $this->typeResolver->findScalarType($itemProperty->type, $itemProperty->format); $propertyDefinition->setScalarTypeId($scalarTypeId); + + if ($this->typeResolver->isDateTime($scalarTypeId) && $this->dateTimeClass !== null) { + $propertyDefinition->setOutputType($this->dateTimeClass); + } } elseif ($itemProperty->type === Type::OBJECT) { $objectType = $this->getObjectSchema( $itemProperty, diff --git a/src/Types/ScalarTypesResolver.php b/src/Types/ScalarTypesResolver.php index f48fd43c..d9900b25 100644 --- a/src/Types/ScalarTypesResolver.php +++ b/src/Types/ScalarTypesResolver.php @@ -6,6 +6,7 @@ use cebe\openapi\spec\Type; +use function dd; use function is_string; use function Safe\settype; @@ -71,7 +72,7 @@ public function __construct() * * @return mixed */ - public function convert(bool $deserialize, int $id, $value) + public function convert(bool $deserialize, int $id, $value, ?object $outputClass = null) { if ($value === null) { return null; @@ -80,6 +81,10 @@ public function convert(bool $deserialize, int $id, $value) $format = $this->scalarTypes[$id]; if ($deserialize && isset($format['deserializer'])) { + if ($outputClass !== null) { + return TypeSerializer::{$format['deserializer']}($value, $outputClass); + } + return TypeSerializer::{$format['deserializer']}($value); } diff --git a/src/Types/TypeSerializer.php b/src/Types/TypeSerializer.php index 654b7d8d..4df2cd7c 100644 --- a/src/Types/TypeSerializer.php +++ b/src/Types/TypeSerializer.php @@ -5,24 +5,37 @@ namespace OnMoon\OpenApiServerBundle\Types; use DateTime; +use DateTimeInterface; use function base64_encode; use function Safe\base64_decode; class TypeSerializer { - public static function deserializeDate(string $date): DateTime + private const DESERIALIZATION_DATE_FORMAT = 'Y-m-d'; + private const SERIALIZATION_DATE_FORMAT = 'c'; + + + public static function deserializeDate(string $date, ?DateTimeInterface $dateTimeClass = null): DateTimeInterface { - return \Safe\DateTime::createFromFormat('Y-m-d', $date); + if ($dateTimeClass !== null) { + return $dateTimeClass::createFromFormat(self::DESERIALIZATION_DATE_FORMAT, $date); + } + + return \Safe\DateTime::createFromFormat(self::DESERIALIZATION_DATE_FORMAT, $date); } public static function serializeDate(DateTime $date): string { - return $date->format('Y-m-d'); + return $date->format(self::SERIALIZATION_DATE_FORMAT); } - public static function deserializeDateTime(string $date): DateTime + public static function deserializeDateTime(string $date, ?DateTimeInterface $dateTimeClass = null): DateTimeInterface { + if ($dateTimeClass !== null) { + return new $dateTimeClass($date); + } + return new \Safe\DateTime($date); } From c22d322794cf5a537b1bb88d33cf0bc3603c8b76 Mon Sep 17 00:00:00 2001 From: Marat Salakhov Date: Mon, 23 Oct 2023 14:49:15 +0300 Subject: [PATCH 2/8] fixed time serialize format --- src/Types/ScalarTypesResolver.php | 1 - src/Types/TypeSerializer.php | 13 ++++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Types/ScalarTypesResolver.php b/src/Types/ScalarTypesResolver.php index d9900b25..5bc550fd 100644 --- a/src/Types/ScalarTypesResolver.php +++ b/src/Types/ScalarTypesResolver.php @@ -6,7 +6,6 @@ use cebe\openapi\spec\Type; -use function dd; use function is_string; use function Safe\settype; diff --git a/src/Types/TypeSerializer.php b/src/Types/TypeSerializer.php index 4df2cd7c..9ab6e6e2 100644 --- a/src/Types/TypeSerializer.php +++ b/src/Types/TypeSerializer.php @@ -12,22 +12,21 @@ class TypeSerializer { - private const DESERIALIZATION_DATE_FORMAT = 'Y-m-d'; - private const SERIALIZATION_DATE_FORMAT = 'c'; - + private const DATE_FORMAT = 'Y-m-d'; + private const DATETIME_FORMAT = 'c'; public static function deserializeDate(string $date, ?DateTimeInterface $dateTimeClass = null): DateTimeInterface { if ($dateTimeClass !== null) { - return $dateTimeClass::createFromFormat(self::DESERIALIZATION_DATE_FORMAT, $date); + return $dateTimeClass::createFromFormat(self::DATE_FORMAT, $date); } - return \Safe\DateTime::createFromFormat(self::DESERIALIZATION_DATE_FORMAT, $date); + return \Safe\DateTime::createFromFormat(self::DATE_FORMAT, $date); } public static function serializeDate(DateTime $date): string { - return $date->format(self::SERIALIZATION_DATE_FORMAT); + return $date->format(self::DATE_FORMAT); } public static function deserializeDateTime(string $date, ?DateTimeInterface $dateTimeClass = null): DateTimeInterface @@ -41,7 +40,7 @@ public static function deserializeDateTime(string $date, ?DateTimeInterface $dat public static function serializeDateTime(DateTime $date): string { - return $date->format('c'); + return $date->format(self::DATETIME_FORMAT); } public static function deserializeByte(string $data): string From f37df7e9df238bbe839973892d0f91d1b156c21d Mon Sep 17 00:00:00 2001 From: Marat Salakhov Date: Tue, 5 Dec 2023 11:35:51 +0300 Subject: [PATCH 3/8] Refactoring and tests --- .../PhpParserGenerators/CodeGenerator.php | 4 + src/Exception/CannotParseOpenApi.php | 5 + src/Specification/Definitions/Property.php | 6 +- src/Specification/SpecificationParser.php | 18 +- src/Types/ScalarTypesResolver.php | 8 +- src/Types/TypeSerializer.php | 48 +++- .../OpenApiServerExtensionTest.php | 16 +- .../PhpParserGenerators/CodeGeneratorTest.php | 12 + .../Specification/SpecificationParserTest.php | 220 ++++++++++++++++++ test/unit/Types/TypeSerializerTest.php | 42 ++++ 10 files changed, 358 insertions(+), 21 deletions(-) diff --git a/src/CodeGenerator/PhpParserGenerators/CodeGenerator.php b/src/CodeGenerator/PhpParserGenerators/CodeGenerator.php index 5211e874..be417a18 100644 --- a/src/CodeGenerator/PhpParserGenerators/CodeGenerator.php +++ b/src/CodeGenerator/PhpParserGenerators/CodeGenerator.php @@ -63,6 +63,10 @@ public function getTypeName(FileBuilder $builder, PropertyDefinition $definition return $builder->getReference($objectType); } + if ($definition->getSpecProperty()->getOutputType() !== null) { + return $definition->getSpecProperty()->getOutputType(); + } + if ($scalarType === null) { throw new Exception('One of ObjectTypeDefinition and ScalarTypeId should not be null'); } diff --git a/src/Exception/CannotParseOpenApi.php b/src/Exception/CannotParseOpenApi.php index 809f3b59..3d979136 100644 --- a/src/Exception/CannotParseOpenApi.php +++ b/src/Exception/CannotParseOpenApi.php @@ -112,4 +112,9 @@ public static function becauseTypeNotSupported(string $propertyName, string $typ ) ); } + + public static function becauseUnknownType(string $name): self + { + return new self(sprintf('Class "%s" does not exist', $name)); + } } diff --git a/src/Specification/Definitions/Property.php b/src/Specification/Definitions/Property.php index 964d2cdb..3063da0b 100644 --- a/src/Specification/Definitions/Property.php +++ b/src/Specification/Definitions/Property.php @@ -17,7 +17,7 @@ final class Property private ObjectSchema|ObjectReference|null $objectTypeDefinition = null; private ?string $description = null; private ?string $pattern = null; - private ?object $outputType = null; + private ?string $outputType = null; public function __construct(string $name) { @@ -149,12 +149,12 @@ public function setNullable(bool $nullable): self return $this; } - public function getOutputType(): ?object + public function getOutputType(): ?string { return $this->outputType; } - public function setOutputType(?object $outputType): self + public function setOutputType(?string $outputType): self { $this->outputType = $outputType; diff --git a/src/Specification/SpecificationParser.php b/src/Specification/SpecificationParser.php index b6154139..f3b7b04d 100644 --- a/src/Specification/SpecificationParser.php +++ b/src/Specification/SpecificationParser.php @@ -31,8 +31,10 @@ use function array_key_exists; use function array_map; use function array_merge; +use function class_exists; use function count; use function in_array; +use function is_a; use function is_array; use function is_int; use function Safe\preg_match; @@ -45,7 +47,7 @@ class SpecificationParser private ScalarTypesResolver $typeResolver; /** @var string[] */ private array $skipHttpCodes; - private ?DateTimeInterface $dateTimeClass = null; + private ?string $dateTimeClass = null; /** @param array $skipHttpCodes */ public function __construct(ScalarTypesResolver $typeResolver, array $skipHttpCodes) @@ -61,7 +63,7 @@ public function parseOpenApi(string $specificationName, SpecificationConfig $spe $operationDefinitions = []; if ($specificationConfig->getDateTimeClass() !== null) { - $this->dateTimeClass = new ($specificationConfig->getDateTimeClass())(); + $this->dateTimeClass = $specificationConfig->getDateTimeClass(); } /** @@ -393,6 +395,18 @@ private function getProperty( $propertyDefinition->setScalarTypeId($scalarTypeId); if ($this->typeResolver->isDateTime($scalarTypeId) && $this->dateTimeClass !== null) { + if (! class_exists($this->dateTimeClass)) { + throw CannotParseOpenApi::becauseUnknownType($this->dateTimeClass); + } + + if (is_a($this->dateTimeClass, DateTimeInterface::class, true) === false) { + throw CannotParseOpenApi::becauseTypeNotSupported( + $propertyName, + $this->dateTimeClass, + $exceptionContext + ); + } + $propertyDefinition->setOutputType($this->dateTimeClass); } } elseif ($itemProperty->type === Type::OBJECT) { diff --git a/src/Types/ScalarTypesResolver.php b/src/Types/ScalarTypesResolver.php index 5bc550fd..5b17ab52 100644 --- a/src/Types/ScalarTypesResolver.php +++ b/src/Types/ScalarTypesResolver.php @@ -71,7 +71,7 @@ public function __construct() * * @return mixed */ - public function convert(bool $deserialize, int $id, $value, ?object $outputClass = null) + public function convert(bool $deserialize, int $id, $value, ?string $outputClass = null) { if ($value === null) { return null; @@ -80,11 +80,7 @@ public function convert(bool $deserialize, int $id, $value, ?object $outputClass $format = $this->scalarTypes[$id]; if ($deserialize && isset($format['deserializer'])) { - if ($outputClass !== null) { - return TypeSerializer::{$format['deserializer']}($value, $outputClass); - } - - return TypeSerializer::{$format['deserializer']}($value); + return TypeSerializer::{$format['deserializer']}($value, $outputClass); } if (! $deserialize && isset($format['serializer'])) { diff --git a/src/Types/TypeSerializer.php b/src/Types/TypeSerializer.php index 9ab6e6e2..288f4372 100644 --- a/src/Types/TypeSerializer.php +++ b/src/Types/TypeSerializer.php @@ -6,22 +6,48 @@ use DateTime; use DateTimeInterface; +use Exception; +use Safe\Exceptions\DatetimeException; use function base64_encode; +use function error_get_last; +use function method_exists; use function Safe\base64_decode; +use function sprintf; class TypeSerializer { private const DATE_FORMAT = 'Y-m-d'; private const DATETIME_FORMAT = 'c'; - public static function deserializeDate(string $date, ?DateTimeInterface $dateTimeClass = null): DateTimeInterface + /** + * @psalm-param class-string $dateTimeClass + * + * @template T of DateTimeInterface + */ + public static function deserializeDate(string $date, ?string $dateTimeClass = null): DateTimeInterface { - if ($dateTimeClass !== null) { - return $dateTimeClass::createFromFormat(self::DATE_FORMAT, $date); + if ($dateTimeClass === null) { + return \Safe\DateTime::createFromFormat(self::DATE_FORMAT, $date); } - return \Safe\DateTime::createFromFormat(self::DATE_FORMAT, $date); + if (method_exists($dateTimeClass, 'createFromFormat') === false) { + throw new Exception(sprintf( + 'Method createFromFormat does not exist in class %s', + $dateTimeClass + )); + } + + /** @psalm-suppress UndefinedMethod */ + $deserializedDate = $dateTimeClass::createFromFormat(self::DATE_FORMAT, $date); + + if ($deserializedDate === false) { + $error = error_get_last(); + + throw new DatetimeException($error['message'] ?? 'An error occurred'); + } + + return $deserializedDate; } public static function serializeDate(DateTime $date): string @@ -29,13 +55,19 @@ public static function serializeDate(DateTime $date): string return $date->format(self::DATE_FORMAT); } - public static function deserializeDateTime(string $date, ?DateTimeInterface $dateTimeClass = null): DateTimeInterface + /** + * @psalm-param class-string $dateTimeClass + * + * @template T of DateTimeInterface + */ + public static function deserializeDateTime(string $date, ?string $dateTimeClass = null): DateTimeInterface { - if ($dateTimeClass !== null) { - return new $dateTimeClass($date); + if ($dateTimeClass === null) { + return new \Safe\DateTime($date); } - return new \Safe\DateTime($date); + /** @psalm-suppress InvalidStringClass */ + return new $dateTimeClass($date); } public static function serializeDateTime(DateTime $date): string diff --git a/test/functional/DependencyInjection/OpenApiServerExtensionTest.php b/test/functional/DependencyInjection/OpenApiServerExtensionTest.php index 68530f70..40aac38d 100644 --- a/test/functional/DependencyInjection/OpenApiServerExtensionTest.php +++ b/test/functional/DependencyInjection/OpenApiServerExtensionTest.php @@ -70,7 +70,14 @@ public function testLoadServiceDefinitionWithMethodCall(): void 'generated_dir_permissions' => '0444', 'full_doc_blocks' => true, 'send_nulls' => true, - 'specs' => [['path' => 'test', 'name_space' => 'test', 'media_type' => 'application/json']], + 'specs' => [ + [ + 'path' => 'test', + 'name_space' => 'test', + 'media_type' => 'application/json', + 'date_time_class' => 'TestClass', + ], + ], ]); $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( @@ -78,7 +85,12 @@ public function testLoadServiceDefinitionWithMethodCall(): void 'registerSpec', [ 0, - ['path' => 'test', 'name_space' => 'test', 'media_type' => 'application/json'], + [ + 'path' => 'test', + 'name_space' => 'test', + 'media_type' => 'application/json', + 'date_time_class' => 'TestClass', + ], ] ); } diff --git a/test/unit/CodeGenerator/PhpParserGenerators/CodeGeneratorTest.php b/test/unit/CodeGenerator/PhpParserGenerators/CodeGeneratorTest.php index 07cda2ac..bc1add5b 100644 --- a/test/unit/CodeGenerator/PhpParserGenerators/CodeGeneratorTest.php +++ b/test/unit/CodeGenerator/PhpParserGenerators/CodeGeneratorTest.php @@ -85,6 +85,18 @@ public function testGetTypeNameReturnsClassViaObjectType(): void Assert::assertEquals($expectedClassName, $typeName); } + public function testGetTypeNameReturnsCustomDateTimeClass(): void + { + $expectedClassName = 'TestCustomDateTimeClass'; + $property = new Property('test'); + $property->setOutputType($expectedClassName); + $propertyDefinition = new PropertyDefinition($property); + $fileBuilderMock = $this->createMock(FileBuilder::class); + $typeName = $this->codeGenerator->getTypeName($fileBuilderMock, $propertyDefinition); + + Assert::assertEquals($expectedClassName, $typeName); + } + public function testGetTypeNameThrowsException(): void { $propertyDefinition = new PropertyDefinition(new Property('test')); diff --git a/test/unit/Specification/SpecificationParserTest.php b/test/unit/Specification/SpecificationParserTest.php index eda7f8b0..67d4d9ac 100644 --- a/test/unit/Specification/SpecificationParserTest.php +++ b/test/unit/Specification/SpecificationParserTest.php @@ -15,6 +15,7 @@ use cebe\openapi\spec\Responses; use cebe\openapi\spec\Schema; use cebe\openapi\spec\Type; +use DateTimeImmutable; use OnMoon\OpenApiServerBundle\Exception\CannotParseOpenApi; use OnMoon\OpenApiServerBundle\Specification\Definitions\ObjectSchema; use OnMoon\OpenApiServerBundle\Specification\Definitions\SpecificationConfig; @@ -22,8 +23,10 @@ use OnMoon\OpenApiServerBundle\Types\ScalarTypesResolver; use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; +use stdClass; use function array_map; +use function sprintf; /** * @covers \OnMoon\OpenApiServerBundle\Specification\SpecificationParser @@ -288,6 +291,80 @@ public function testParseOpenApiSuccess(): void ); } + public function testParseOpenApiWithCustomDateTimeClassSuccess(): void + { + $specificationName = 'SomeCustomSpecification'; + $someDateTimeClass = $this->createMock(DateTimeImmutable::class); + $specificationConfig = new SpecificationConfig( + '/some/custom/specification/path', + null, + '\\Some\\Custom\\Namespace', + 'application/json', + $someDateTimeClass::class + ); + $parsedSpecification = new OpenApi([ + 'paths' => new Paths([ + '/some/custom/url' => [ + 'post' => new Operation([ + 'operationId' => 'SomeCustomOperationWithRequestAndResponses', + 'requestBody' => new RequestBody([ + 'description' => 'SomeCustomRequestParam', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + 'responses' => new Responses([ + '200' => new Response([ + 'description' => 'SomeCustomResponseParam200', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + ]), + ]), + ], + ]), + ]); + + $specificationParser = new SpecificationParser(new ScalarTypesResolver(), []); + + $specification = $specificationParser->parseOpenApi( + $specificationName, + $specificationConfig, + $parsedSpecification + ); + + $requestBody = $specification + ->getOperation('SomeCustomOperationWithRequestAndResponses') + ->getRequestBody(); + Assert::assertNotNull($requestBody); + + $requestBodyProperties = $requestBody->getProperties(); + Assert::assertSame($someDateTimeClass::class, $requestBodyProperties[0]->getOutputType()); + } + public function testParseOpenApiSuccessRequestBadMediaType(): void { $specificationName = 'SomeCustomSpecification'; @@ -927,4 +1004,147 @@ public function testParseOpenApiThrowExceptionDuplicateOperationId(): void $parsedSpecification ); } + + public function testParseOpenApiWithCustomDateTimeClassThrowExceptionUnknownType(): void + { + $specificationName = 'SomeCustomSpecification'; + $specificationConfig = new SpecificationConfig( + '/some/custom/specification/path', + null, + '\\Some\\Custom\\Namespace', + 'application/json', + 'SomeNotExistedDateTimeClass' + ); + $parsedSpecification = new OpenApi([ + 'paths' => new Paths([ + '/some/custom/url' => [ + 'post' => new Operation([ + 'operationId' => 'SomeCustomOperationWithRequestAndResponses', + 'requestBody' => new RequestBody([ + 'description' => 'SomeCustomRequestParam', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + 'responses' => new Responses([ + '200' => new Response([ + 'description' => 'SomeCustomResponseParam200', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + ]), + ]), + ], + ]), + ]); + + $specificationParser = new SpecificationParser(new ScalarTypesResolver(), []); + + $this->expectException(CannotParseOpenApi::class); + $this->expectExceptionMessage('Class "SomeNotExistedDateTimeClass" does not exist'); + + $specificationParser->parseOpenApi( + $specificationName, + $specificationConfig, + $parsedSpecification + ); + } + + public function testParseOpenApiWithCustomDateTimeClassThrowExceptionTypeNotSupported(): void + { + $specificationName = 'SomeCustomSpecification'; + $someNotDateTimeClass = $this->getMockBuilder(stdClass::class)->getMock(); + $specificationConfig = new SpecificationConfig( + '/some/custom/specification/path', + null, + '\\Some\\Custom\\Namespace', + 'application/json', + $someNotDateTimeClass::class + ); + $parsedSpecification = new OpenApi([ + 'paths' => new Paths([ + '/some/custom/url' => [ + 'post' => new Operation([ + 'operationId' => 'SomeCustomOperationWithRequestAndResponses', + 'requestBody' => new RequestBody([ + 'description' => 'SomeCustomRequestParam', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + 'responses' => new Responses([ + '200' => new Response([ + 'description' => 'SomeCustomResponseParam200', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + ]), + ]), + ], + ]), + ]); + + $specificationParser = new SpecificationParser(new ScalarTypesResolver(), []); + + $this->expectException(CannotParseOpenApi::class); + $this->expectExceptionMessage(sprintf( + 'Cannot generate property for DTO class, property "someDateTimeProperty" type "%s" is not supported ' . + 'in response (code "200") for operation: "post" of path: "/some/custom/url" in specification file: ' . + '"/some/custom/specification/path".', + $someNotDateTimeClass::class + )); + + $specificationParser->parseOpenApi( + $specificationName, + $specificationConfig, + $parsedSpecification + ); + } + } diff --git a/test/unit/Types/TypeSerializerTest.php b/test/unit/Types/TypeSerializerTest.php index 7001c20c..33458bdb 100644 --- a/test/unit/Types/TypeSerializerTest.php +++ b/test/unit/Types/TypeSerializerTest.php @@ -5,12 +5,15 @@ namespace OnMoon\OpenApiServerBundle\Test\Unit\Types; use DateTime; +use DateTimeImmutable; use OnMoon\OpenApiServerBundle\Types\TypeSerializer; use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use Safe\Exceptions\DatetimeException; use Throwable; +use function sprintf; + /** * @covers \OnMoon\OpenApiServerBundle\Types\TypeSerializer */ @@ -24,6 +27,14 @@ public function testDeserializeDateReturnsDateTime(): void Assert::assertEquals($expectedDate, $deserializedDate); } + public function testDeserializeDateWithCustomDateTimeClassReturnsDateTime(): void + { + $dateString = '2020-05-12'; + $expectedDate = DateTime::createFromFormat('Y-m-d', $dateString); + $deserializedDate = TypeSerializer::deserializeDate($dateString, DateTimeImmutable::class); + Assert::assertEquals($expectedDate, $deserializedDate); + } + public function testDeserializeDateThrowsException(): void { $dateString = '22-07-2020'; @@ -31,6 +42,29 @@ public function testDeserializeDateThrowsException(): void TypeSerializer::deserializeDate($dateString); } + public function testDeserializeDateWithCustomDateTimeClassThrowsException(): void + { + $dateString = '22-07-2020'; + $this->expectException(DatetimeException::class); + TypeSerializer::deserializeDate($dateString, DateTimeImmutable::class); + } + + public function testDeserializeDateWithCustomDateTimeClassThrowsNotExistMethodException(): void + { + $dateString = '2020-05-12'; + $wrongClass = $this + ->getMockBuilder(DateTimeImmutable::class) + ->setMockClassName('WrongClass') + ->disableProxyingToOriginalMethods(); + + $this->expectException(Throwable::class); + $this->expectExceptionMessage(sprintf( + 'Method createFromFormat does not exist in class %s', + $wrongClass::class + )); + TypeSerializer::deserializeDate($dateString, $wrongClass::class); + } + public function testSerializeDateReturnsSerializedDate(): void { $dateString = '25-12-2020'; @@ -48,6 +82,14 @@ public function testDeserializeDateTimeReturnsDateTime(): void Assert::assertEquals($expectedDateTime, $deserializedDateTime); } + public function testDeserializeDateTimeWithCustomDateTimeClassReturnsDateTime(): void + { + $dateString = '25-12-2020 12:01:55'; + $expectedDateTime = new \Safe\DateTime($dateString); + $deserializedDateTime = TypeSerializer::deserializeDateTime($dateString, DateTimeImmutable::class); + Assert::assertEquals($expectedDateTime, $deserializedDateTime); + } + public function testDeserializeDateTimeThrowsException(): void { $dateString = 'wrong date string'; From 1508ec8d3b20ce1cc39393061370cc9da77997da Mon Sep 17 00:00:00 2001 From: Marat Salakhov Date: Tue, 5 Dec 2023 12:22:02 +0300 Subject: [PATCH 4/8] Code style fixes --- test/unit/Specification/SpecificationParserTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/unit/Specification/SpecificationParserTest.php b/test/unit/Specification/SpecificationParserTest.php index 67d4d9ac..f2dec68a 100644 --- a/test/unit/Specification/SpecificationParserTest.php +++ b/test/unit/Specification/SpecificationParserTest.php @@ -294,13 +294,12 @@ public function testParseOpenApiSuccess(): void public function testParseOpenApiWithCustomDateTimeClassSuccess(): void { $specificationName = 'SomeCustomSpecification'; - $someDateTimeClass = $this->createMock(DateTimeImmutable::class); $specificationConfig = new SpecificationConfig( '/some/custom/specification/path', null, '\\Some\\Custom\\Namespace', 'application/json', - $someDateTimeClass::class + DateTimeImmutable::class ); $parsedSpecification = new OpenApi([ 'paths' => new Paths([ @@ -362,7 +361,7 @@ public function testParseOpenApiWithCustomDateTimeClassSuccess(): void Assert::assertNotNull($requestBody); $requestBodyProperties = $requestBody->getProperties(); - Assert::assertSame($someDateTimeClass::class, $requestBodyProperties[0]->getOutputType()); + Assert::assertSame(DateTimeImmutable::class, $requestBodyProperties[0]->getOutputType()); } public function testParseOpenApiSuccessRequestBadMediaType(): void @@ -1146,5 +1145,4 @@ public function testParseOpenApiWithCustomDateTimeClassThrowExceptionTypeNotSupp $parsedSpecification ); } - } From 8298b6c584a8141891203ff94678730b526b8305 Mon Sep 17 00:00:00 2001 From: Marat Salakhov Date: Tue, 5 Dec 2023 15:13:31 +0300 Subject: [PATCH 5/8] Fixed mutation test --- src/Serializer/ArrayDtoSerializer.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Serializer/ArrayDtoSerializer.php b/src/Serializer/ArrayDtoSerializer.php index 93641898..3d4fe9c2 100644 --- a/src/Serializer/ArrayDtoSerializer.php +++ b/src/Serializer/ArrayDtoSerializer.php @@ -113,10 +113,7 @@ private function convert(bool $deserialize, array $source, ObjectSchema $params) /** @psalm-suppress MissingClosureParamType */ $converter = fn ($v) => $this->convert($deserialize, $v, $objectType->getSchema()); } else { - $outputClass = null; - if ($typeId !== null && $this->resolver->isDateTime($typeId)) { - $outputClass = $property->getOutputType(); - } + $outputClass = $property->getOutputType(); /** @psalm-suppress MissingClosureParamType */ $converter = fn ($v) => $this->resolver->convert($deserialize, $typeId ?? 0, $v, $outputClass); From 681eda6394bc692e86e39dc0761307f5e8c69938 Mon Sep 17 00:00:00 2001 From: Marat Salakhov Date: Tue, 5 Dec 2023 17:32:04 +0300 Subject: [PATCH 6/8] mutation test --- test/unit/CodeGenerator/AttributeGeneratorTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/CodeGenerator/AttributeGeneratorTest.php b/test/unit/CodeGenerator/AttributeGeneratorTest.php index 06eb8050..7e9dfbaf 100644 --- a/test/unit/CodeGenerator/AttributeGeneratorTest.php +++ b/test/unit/CodeGenerator/AttributeGeneratorTest.php @@ -195,9 +195,9 @@ public function testRequestPassDefault(): void $property->setDefaultValue('test'); $propertyTwo = new Property('two'); - $propertyTwo->setNullable(true); - $propertyTwo->setRequired(true); - $propertyTwo->setDefaultValue('testTwo'); + $propertyTwo->setNullable(false); + $propertyTwo->setRequired(false); + $propertyTwo->setDefaultValue(null); $propertyDefinition = new PropertyDefinition($property); $propertyDefinitionTwo = new PropertyDefinition($propertyTwo); From f0affbc998f0cafa1b73e340ff27627f0a3ef9ba Mon Sep 17 00:00:00 2001 From: Marat Salakhov Date: Tue, 19 Dec 2023 00:37:12 +0500 Subject: [PATCH 7/8] Fixed FQCN bug and multiple custom datetime class bug. Added tests --- src/Exception/CannotParseOpenApi.php | 5 + src/Specification/SpecificationParser.php | 8 +- .../Specification/SpecificationParserTest.php | 140 +++++++++++++++--- 3 files changed, 128 insertions(+), 25 deletions(-) diff --git a/src/Exception/CannotParseOpenApi.php b/src/Exception/CannotParseOpenApi.php index 3d979136..4e08891c 100644 --- a/src/Exception/CannotParseOpenApi.php +++ b/src/Exception/CannotParseOpenApi.php @@ -117,4 +117,9 @@ public static function becauseUnknownType(string $name): self { return new self(sprintf('Class "%s" does not exist', $name)); } + + public static function becauseNotFQCN(string $name): self + { + return new self(sprintf('Class "%s" should have fully qualified name', $name)); + } } diff --git a/src/Specification/SpecificationParser.php b/src/Specification/SpecificationParser.php index f3b7b04d..65ba4797 100644 --- a/src/Specification/SpecificationParser.php +++ b/src/Specification/SpecificationParser.php @@ -62,9 +62,7 @@ public function parseOpenApi(string $specificationName, SpecificationConfig $spe $operationDefinitions = []; - if ($specificationConfig->getDateTimeClass() !== null) { - $this->dateTimeClass = $specificationConfig->getDateTimeClass(); - } + $this->dateTimeClass = $specificationConfig->getDateTimeClass(); /** * @var string $url @@ -395,6 +393,10 @@ private function getProperty( $propertyDefinition->setScalarTypeId($scalarTypeId); if ($this->typeResolver->isDateTime($scalarTypeId) && $this->dateTimeClass !== null) { + if (preg_match('/^\\\\/', $this->dateTimeClass) !== 1) { + throw CannotParseOpenApi::becauseNotFQCN($this->dateTimeClass); + } + if (! class_exists($this->dateTimeClass)) { throw CannotParseOpenApi::becauseUnknownType($this->dateTimeClass); } diff --git a/test/unit/Specification/SpecificationParserTest.php b/test/unit/Specification/SpecificationParserTest.php index f2dec68a..c43b9dc1 100644 --- a/test/unit/Specification/SpecificationParserTest.php +++ b/test/unit/Specification/SpecificationParserTest.php @@ -15,7 +15,6 @@ use cebe\openapi\spec\Responses; use cebe\openapi\spec\Schema; use cebe\openapi\spec\Type; -use DateTimeImmutable; use OnMoon\OpenApiServerBundle\Exception\CannotParseOpenApi; use OnMoon\OpenApiServerBundle\Specification\Definitions\ObjectSchema; use OnMoon\OpenApiServerBundle\Specification\Definitions\SpecificationConfig; @@ -23,7 +22,6 @@ use OnMoon\OpenApiServerBundle\Types\ScalarTypesResolver; use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; -use stdClass; use function array_map; use function sprintf; @@ -293,15 +291,24 @@ public function testParseOpenApiSuccess(): void public function testParseOpenApiWithCustomDateTimeClassSuccess(): void { - $specificationName = 'SomeCustomSpecification'; - $specificationConfig = new SpecificationConfig( - '/some/custom/specification/path', + $customDateTimeClass = '\DateTimeImmutable'; + $specificationNameOne = 'SomeCustomSpecificationOne'; + $specificationConfigOne = new SpecificationConfig( + '/some/custom/specification/one/path', null, '\\Some\\Custom\\Namespace', 'application/json', - DateTimeImmutable::class + $customDateTimeClass ); - $parsedSpecification = new OpenApi([ + $specificationNameTwo = 'SomeCustomSpecificationTwo'; + $specificationConfigTwo = new SpecificationConfig( + '/some/custom/specification/two/path', + null, + '\\Some\\Custom\\Namespace', + 'application/json', + null + ); + $parsedSpecification = new OpenApi([ 'paths' => new Paths([ '/some/custom/url' => [ 'post' => new Operation([ @@ -349,19 +356,32 @@ public function testParseOpenApiWithCustomDateTimeClassSuccess(): void $specificationParser = new SpecificationParser(new ScalarTypesResolver(), []); - $specification = $specificationParser->parseOpenApi( - $specificationName, - $specificationConfig, + $specificationOne = $specificationParser->parseOpenApi( + $specificationNameOne, + $specificationConfigOne, $parsedSpecification ); - $requestBody = $specification + $specificationOneRequestBody = $specificationOne ->getOperation('SomeCustomOperationWithRequestAndResponses') ->getRequestBody(); - Assert::assertNotNull($requestBody); + Assert::assertNotNull($specificationOneRequestBody); - $requestBodyProperties = $requestBody->getProperties(); - Assert::assertSame(DateTimeImmutable::class, $requestBodyProperties[0]->getOutputType()); + $specificationOneRequestBodyProperties = $specificationOneRequestBody->getProperties(); + Assert::assertSame($customDateTimeClass, $specificationOneRequestBodyProperties[0]->getOutputType()); + + $specificationTwo = $specificationParser->parseOpenApi( + $specificationNameTwo, + $specificationConfigTwo, + $parsedSpecification + ); + $specificationTwoRequestBody = $specificationTwo + ->getOperation('SomeCustomOperationWithRequestAndResponses') + ->getRequestBody(); + Assert::assertNotNull($specificationTwoRequestBody); + + $specificationTwoRequestBodyProperties = $specificationTwoRequestBody->getProperties(); + Assert::assertNull($specificationTwoRequestBodyProperties[0]->getOutputType()); } public function testParseOpenApiSuccessRequestBadMediaType(): void @@ -1006,15 +1026,16 @@ public function testParseOpenApiThrowExceptionDuplicateOperationId(): void public function testParseOpenApiWithCustomDateTimeClassThrowExceptionUnknownType(): void { - $specificationName = 'SomeCustomSpecification'; - $specificationConfig = new SpecificationConfig( + $someNotExistedDateTimeClass = '\SomeNotExistedDateTimeClass'; + $specificationName = 'SomeCustomSpecification'; + $specificationConfig = new SpecificationConfig( '/some/custom/specification/path', null, '\\Some\\Custom\\Namespace', 'application/json', - 'SomeNotExistedDateTimeClass' + $someNotExistedDateTimeClass ); - $parsedSpecification = new OpenApi([ + $parsedSpecification = new OpenApi([ 'paths' => new Paths([ '/some/custom/url' => [ 'post' => new Operation([ @@ -1063,7 +1084,10 @@ public function testParseOpenApiWithCustomDateTimeClassThrowExceptionUnknownType $specificationParser = new SpecificationParser(new ScalarTypesResolver(), []); $this->expectException(CannotParseOpenApi::class); - $this->expectExceptionMessage('Class "SomeNotExistedDateTimeClass" does not exist'); + $this->expectExceptionMessage(sprintf( + 'Class "%s" does not exist', + $someNotExistedDateTimeClass + )); $specificationParser->parseOpenApi( $specificationName, @@ -1075,13 +1099,13 @@ public function testParseOpenApiWithCustomDateTimeClassThrowExceptionUnknownType public function testParseOpenApiWithCustomDateTimeClassThrowExceptionTypeNotSupported(): void { $specificationName = 'SomeCustomSpecification'; - $someNotDateTimeClass = $this->getMockBuilder(stdClass::class)->getMock(); + $someNotDateTimeClass = '\stdClass'; $specificationConfig = new SpecificationConfig( '/some/custom/specification/path', null, '\\Some\\Custom\\Namespace', 'application/json', - $someNotDateTimeClass::class + $someNotDateTimeClass ); $parsedSpecification = new OpenApi([ 'paths' => new Paths([ @@ -1136,7 +1160,79 @@ public function testParseOpenApiWithCustomDateTimeClassThrowExceptionTypeNotSupp 'Cannot generate property for DTO class, property "someDateTimeProperty" type "%s" is not supported ' . 'in response (code "200") for operation: "post" of path: "/some/custom/url" in specification file: ' . '"/some/custom/specification/path".', - $someNotDateTimeClass::class + $someNotDateTimeClass + )); + + $specificationParser->parseOpenApi( + $specificationName, + $specificationConfig, + $parsedSpecification + ); + } + + public function testParseOpenApiWithCustomDateTimeClassThrowExceptionNotFQCN(): void + { + $someNotFqcnClassName = 'SomeNotFqcnClassName'; + $specificationName = 'SomeCustomSpecification'; + $specificationConfig = new SpecificationConfig( + '/some/custom/specification/path', + null, + '\\Some\\Custom\\Namespace', + 'application/json', + $someNotFqcnClassName + ); + $parsedSpecification = new OpenApi([ + 'paths' => new Paths([ + '/some/custom/url' => [ + 'post' => new Operation([ + 'operationId' => 'SomeCustomOperationWithRequestAndResponses', + 'requestBody' => new RequestBody([ + 'description' => 'SomeCustomRequestParam', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + 'responses' => new Responses([ + '200' => new Response([ + 'description' => 'SomeCustomResponseParam200', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + ]), + ]), + ], + ]), + ]); + + $specificationParser = new SpecificationParser(new ScalarTypesResolver(), []); + + $this->expectException(CannotParseOpenApi::class); + $this->expectExceptionMessage(sprintf( + 'Class "%s" should have fully qualified name', + $someNotFqcnClassName )); $specificationParser->parseOpenApi( From 6cbd745d8ad980e8ff9efb929dafe5f598ea9b0c Mon Sep 17 00:00:00 2001 From: Marat Salakhov Date: Wed, 20 Dec 2023 23:21:24 +0500 Subject: [PATCH 8/8] Added settings example in readme file. Changed type in dateTime serialization and deserialization methods --- README.md | 2 ++ src/Types/TypeSerializer.php | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6491ebf5..a634eb62 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ open_api_server: type: yaml # Specification format, either yaml or json. If omitted, the specification file extension will be used. name_space: PetStore # Namespace for generated DTOs and Interfaces media_type: 'application/json' # media type from the specification files to use for generating request and response DTOs + #date_time_class: '\Carbon\CarbonImmutable' # FQCN which implements \DateTimeInterface. + ## If set up, then generated DTOs will return instances of this class in DateTime parameters ``` Add your OpenApi specifications to the application routes configuration file using standard `resource` keyword diff --git a/src/Types/TypeSerializer.php b/src/Types/TypeSerializer.php index 288f4372..a78f7029 100644 --- a/src/Types/TypeSerializer.php +++ b/src/Types/TypeSerializer.php @@ -4,9 +4,9 @@ namespace OnMoon\OpenApiServerBundle\Types; -use DateTime; use DateTimeInterface; use Exception; +use Safe\DateTime; use Safe\Exceptions\DatetimeException; use function base64_encode; @@ -28,7 +28,7 @@ class TypeSerializer public static function deserializeDate(string $date, ?string $dateTimeClass = null): DateTimeInterface { if ($dateTimeClass === null) { - return \Safe\DateTime::createFromFormat(self::DATE_FORMAT, $date); + return DateTime::createFromFormat(self::DATE_FORMAT, $date); } if (method_exists($dateTimeClass, 'createFromFormat') === false) { @@ -50,7 +50,7 @@ public static function deserializeDate(string $date, ?string $dateTimeClass = nu return $deserializedDate; } - public static function serializeDate(DateTime $date): string + public static function serializeDate(DateTimeInterface $date): string { return $date->format(self::DATE_FORMAT); } @@ -63,14 +63,14 @@ public static function serializeDate(DateTime $date): string public static function deserializeDateTime(string $date, ?string $dateTimeClass = null): DateTimeInterface { if ($dateTimeClass === null) { - return new \Safe\DateTime($date); + return new DateTime($date); } /** @psalm-suppress InvalidStringClass */ return new $dateTimeClass($date); } - public static function serializeDateTime(DateTime $date): string + public static function serializeDateTime(DateTimeInterface $date): string { return $date->format(self::DATETIME_FORMAT); }