From 99045b3c3f8de2b00ef908441b84eb18082eccdc Mon Sep 17 00:00:00 2001 From: Dan Burke Date: Fri, 20 Sep 2024 15:25:17 -0400 Subject: [PATCH] feat(php): update serde for backwards compatible constructors (#4710) --- .../src/asIs/DateArrayTypeTest.Template.php | 17 +- .../src/asIs/EmptyArraysTest.Template.php | 49 ++++-- .../src/asIs/InvalidTypesTest.Template.php | 20 ++- .../src/asIs/JsonDeserializer.Template.php | 34 +++- .../src/asIs/JsonSerializer.Template.php | 30 +++- .../asIs/MixedDateArrayTypeTest.Template.php | 22 ++- .../NestedUnionArrayTypeTest.Template.php | 31 +++- .../asIs/NullPropertyTypeTest.Template.php | 27 ++- .../asIs/NullableArrayTypeTest.Template.php | 21 ++- .../src/asIs/ScalarTypesTest.Template.php | 69 ++++++-- .../src/asIs/SerializableType.Template.php | 90 ++++++---- .../src/asIs/TestTypeTest.Template.php | 165 ++++++++++++------ .../php/codegen/src/asIs/Union.Template.php | 9 +- .../src/asIs/UnionArrayTypeTest.Template.php | 22 ++- .../php/codegen/src/asIs/Utils.Template.php | 23 ++- .../src/Core/JsonDeserializer.php | 30 +++- .../alias-extends/src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../alias-extends/src/Core/Union.php | 5 + .../alias-extends/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../alias/src/Core/JsonDeserializer.php | 30 +++- .../alias/src/Core/JsonSerializer.php | 25 ++- .../alias/src/Core/SerializableType.php | 80 +++++---- seed/php-model/alias/src/Core/Union.php | 5 + seed/php-model/alias/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../alias/tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../alias/tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../alias/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../any-auth/src/Core/JsonDeserializer.php | 30 +++- .../any-auth/src/Core/JsonSerializer.php | 25 ++- .../any-auth/src/Core/SerializableType.php | 80 +++++---- seed/php-model/any-auth/src/Core/Union.php | 5 + seed/php-model/any-auth/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../any-auth/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../api-wide-base-path/src/Core/Union.php | 5 + .../api-wide-base-path/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../audiences/src/Core/JsonDeserializer.php | 30 +++- .../audiences/src/Core/JsonSerializer.php | 25 ++- .../audiences/src/Core/SerializableType.php | 80 +++++---- seed/php-model/audiences/src/Core/Union.php | 5 + seed/php-model/audiences/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../basic-auth/src/Core/JsonDeserializer.php | 30 +++- .../basic-auth/src/Core/JsonSerializer.php | 25 ++- .../basic-auth/src/Core/SerializableType.php | 80 +++++---- seed/php-model/basic-auth/src/Core/Union.php | 5 + seed/php-model/basic-auth/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../bytes/src/Core/JsonDeserializer.php | 30 +++- .../bytes/src/Core/JsonSerializer.php | 25 ++- .../bytes/src/Core/SerializableType.php | 80 +++++---- seed/php-model/bytes/src/Core/Union.php | 5 + seed/php-model/bytes/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../bytes/tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../bytes/tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../bytes/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../circular-references/src/Core/Union.php | 5 + .../circular-references/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../custom-auth/src/Core/JsonDeserializer.php | 30 +++- .../custom-auth/src/Core/JsonSerializer.php | 25 ++- .../custom-auth/src/Core/SerializableType.php | 80 +++++---- seed/php-model/custom-auth/src/Core/Union.php | 5 + seed/php-model/custom-auth/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../enum/src/Core/JsonDeserializer.php | 30 +++- .../enum/src/Core/JsonSerializer.php | 25 ++- .../enum/src/Core/SerializableType.php | 80 +++++---- seed/php-model/enum/src/Core/Union.php | 5 + seed/php-model/enum/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../enum/tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../enum/tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../enum/tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../enum/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../error-property/src/Core/Union.php | 5 + .../error-property/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../examples/src/Core/JsonDeserializer.php | 30 +++- .../examples/src/Core/JsonSerializer.php | 25 ++- .../examples/src/Core/SerializableType.php | 80 +++++---- seed/php-model/examples/src/Core/Union.php | 5 + seed/php-model/examples/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../examples/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../exhaustive/src/Core/JsonDeserializer.php | 30 +++- .../exhaustive/src/Core/JsonSerializer.php | 25 ++- .../exhaustive/src/Core/SerializableType.php | 80 +++++---- seed/php-model/exhaustive/src/Core/Union.php | 5 + seed/php-model/exhaustive/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../extends/src/Core/JsonDeserializer.php | 30 +++- .../extends/src/Core/JsonSerializer.php | 25 ++- .../extends/src/Core/SerializableType.php | 80 +++++---- seed/php-model/extends/src/Core/Union.php | 5 + seed/php-model/extends/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../extends/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../extra-properties/src/Core/Union.php | 5 + .../extra-properties/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../file-download/src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../file-download/src/Core/Union.php | 5 + .../file-download/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../file-upload/src/Core/JsonDeserializer.php | 30 +++- .../file-upload/src/Core/JsonSerializer.php | 25 ++- .../file-upload/src/Core/SerializableType.php | 80 +++++---- seed/php-model/file-upload/src/Core/Union.php | 5 + seed/php-model/file-upload/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../folders/src/Core/JsonDeserializer.php | 30 +++- .../folders/src/Core/JsonSerializer.php | 25 ++- .../folders/src/Core/SerializableType.php | 80 +++++---- seed/php-model/folders/src/Core/Union.php | 5 + seed/php-model/folders/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../folders/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../grpc-proto-exhaustive/src/Core/Union.php | 5 + .../grpc-proto-exhaustive/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../grpc-proto/src/Core/JsonDeserializer.php | 30 +++- .../grpc-proto/src/Core/JsonSerializer.php | 25 ++- .../grpc-proto/src/Core/SerializableType.php | 80 +++++---- seed/php-model/grpc-proto/src/Core/Union.php | 5 + seed/php-model/grpc-proto/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../idempotency-headers/src/Core/Union.php | 5 + .../idempotency-headers/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../imdb/src/Core/JsonDeserializer.php | 30 +++- .../imdb/src/Core/JsonSerializer.php | 25 ++- .../imdb/src/Core/SerializableType.php | 80 +++++---- seed/php-model/imdb/src/Core/Union.php | 5 + seed/php-model/imdb/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../imdb/tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../imdb/tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../imdb/tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../imdb/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../literal/src/Core/JsonDeserializer.php | 30 +++- .../literal/src/Core/JsonSerializer.php | 25 ++- .../literal/src/Core/SerializableType.php | 80 +++++---- seed/php-model/literal/src/Core/Union.php | 5 + seed/php-model/literal/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../literal/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../mixed-case/src/Core/JsonDeserializer.php | 30 +++- .../mixed-case/src/Core/JsonSerializer.php | 25 ++- .../mixed-case/src/Core/SerializableType.php | 80 +++++---- seed/php-model/mixed-case/src/Core/Union.php | 5 + seed/php-model/mixed-case/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../mixed-file-directory/src/Core/Union.php | 5 + .../mixed-file-directory/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../multi-line-docs/src/Core/Union.php | 5 + .../multi-line-docs/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../multi-url-environment/src/Core/Union.php | 5 + .../multi-url-environment/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../no-environment/src/Core/Union.php | 5 + .../no-environment/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../object/src/Core/JsonDeserializer.php | 30 +++- .../object/src/Core/JsonSerializer.php | 25 ++- .../object/src/Core/SerializableType.php | 80 +++++---- seed/php-model/object/src/Core/Union.php | 5 + seed/php-model/object/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../object/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../objects-with-imports/src/Core/Union.php | 5 + .../objects-with-imports/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../optional/src/Core/JsonDeserializer.php | 30 +++- .../optional/src/Core/JsonSerializer.php | 25 ++- .../optional/src/Core/SerializableType.php | 80 +++++---- seed/php-model/optional/src/Core/Union.php | 5 + seed/php-model/optional/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../optional/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../package-yml/src/Core/JsonDeserializer.php | 30 +++- .../package-yml/src/Core/JsonSerializer.php | 25 ++- .../package-yml/src/Core/SerializableType.php | 80 +++++---- seed/php-model/package-yml/src/Core/Union.php | 5 + seed/php-model/package-yml/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../pagination/src/Core/JsonDeserializer.php | 30 +++- .../pagination/src/Core/JsonSerializer.php | 25 ++- .../pagination/src/Core/SerializableType.php | 80 +++++---- seed/php-model/pagination/src/Core/Union.php | 5 + seed/php-model/pagination/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../plain-text/src/Core/JsonDeserializer.php | 30 +++- .../plain-text/src/Core/JsonSerializer.php | 25 ++- .../plain-text/src/Core/SerializableType.php | 80 +++++---- seed/php-model/plain-text/src/Core/Union.php | 5 + seed/php-model/plain-text/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../query-parameters/src/Core/Union.php | 5 + .../query-parameters/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../reserved-keywords/src/Core/Union.php | 5 + .../reserved-keywords/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../response-property/src/Core/Union.php | 5 + .../response-property/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../simple-fhir/src/Core/JsonDeserializer.php | 30 +++- .../simple-fhir/src/Core/JsonSerializer.php | 25 ++- .../simple-fhir/src/Core/SerializableType.php | 80 +++++---- seed/php-model/simple-fhir/src/Core/Union.php | 5 + seed/php-model/simple-fhir/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../streaming-parameter/src/Core/Union.php | 5 + .../streaming-parameter/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../streaming/src/Core/JsonDeserializer.php | 30 +++- .../streaming/src/Core/JsonSerializer.php | 25 ++- .../streaming/src/Core/SerializableType.php | 80 +++++---- seed/php-model/streaming/src/Core/Union.php | 5 + seed/php-model/streaming/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../trace/src/Core/JsonDeserializer.php | 30 +++- .../trace/src/Core/JsonSerializer.php | 25 ++- .../trace/src/Core/SerializableType.php | 80 +++++---- seed/php-model/trace/src/Core/Union.php | 5 + seed/php-model/trace/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../trace/tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../trace/tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../trace/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../undiscriminated-unions/src/Core/Union.php | 5 + .../undiscriminated-unions/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../unions/src/Core/JsonDeserializer.php | 30 +++- .../unions/src/Core/JsonSerializer.php | 25 ++- .../unions/src/Core/SerializableType.php | 80 +++++---- seed/php-model/unions/src/Core/Union.php | 5 + seed/php-model/unions/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../unions/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../unknown/src/Core/JsonDeserializer.php | 30 +++- .../unknown/src/Core/JsonSerializer.php | 25 ++- .../unknown/src/Core/SerializableType.php | 80 +++++---- seed/php-model/unknown/src/Core/Union.php | 5 + seed/php-model/unknown/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../unknown/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../validation/src/Core/JsonDeserializer.php | 30 +++- .../validation/src/Core/JsonSerializer.php | 25 ++- .../validation/src/Core/SerializableType.php | 80 +++++---- seed/php-model/validation/src/Core/Union.php | 5 + seed/php-model/validation/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../variables/src/Core/JsonDeserializer.php | 30 +++- .../variables/src/Core/JsonSerializer.php | 25 ++- .../variables/src/Core/SerializableType.php | 80 +++++---- seed/php-model/variables/src/Core/Union.php | 5 + seed/php-model/variables/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../version-no-default/src/Core/Union.php | 5 + .../version-no-default/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../version/src/Core/JsonDeserializer.php | 30 +++- .../version/src/Core/JsonSerializer.php | 25 ++- .../version/src/Core/SerializableType.php | 80 +++++---- seed/php-model/version/src/Core/Union.php | 5 + seed/php-model/version/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../version/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../websocket/src/Core/JsonDeserializer.php | 30 +++- .../websocket/src/Core/JsonSerializer.php | 25 ++- .../websocket/src/Core/SerializableType.php | 80 +++++---- seed/php-model/websocket/src/Core/Union.php | 5 + seed/php-model/websocket/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../alias-extends/src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/alias-extends/src/Core/Union.php | 5 + seed/php-sdk/alias-extends/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../alias/src/Core/JsonDeserializer.php | 30 +++- .../php-sdk/alias/src/Core/JsonSerializer.php | 25 ++- .../alias/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/alias/src/Core/Union.php | 5 + seed/php-sdk/alias/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../alias/tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../alias/tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../alias/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../any-auth/src/Core/JsonDeserializer.php | 30 +++- .../any-auth/src/Core/JsonSerializer.php | 25 ++- .../any-auth/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/any-auth/src/Core/Union.php | 5 + seed/php-sdk/any-auth/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../any-auth/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../api-wide-base-path/src/Core/Union.php | 5 + .../api-wide-base-path/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../audiences/src/Core/JsonDeserializer.php | 30 +++- .../audiences/src/Core/JsonSerializer.php | 25 ++- .../audiences/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/audiences/src/Core/Union.php | 5 + seed/php-sdk/audiences/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../basic-auth/src/Core/JsonDeserializer.php | 30 +++- .../basic-auth/src/Core/JsonSerializer.php | 25 ++- .../basic-auth/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/basic-auth/src/Core/Union.php | 5 + seed/php-sdk/basic-auth/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../bytes/src/Core/JsonDeserializer.php | 30 +++- .../php-sdk/bytes/src/Core/JsonSerializer.php | 25 ++- .../bytes/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/bytes/src/Core/Union.php | 5 + seed/php-sdk/bytes/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../bytes/tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../bytes/tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../bytes/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../circular-references/src/Core/Union.php | 5 + .../circular-references/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../custom-auth/src/Core/JsonDeserializer.php | 30 +++- .../custom-auth/src/Core/JsonSerializer.php | 25 ++- .../custom-auth/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/custom-auth/src/Core/Union.php | 5 + seed/php-sdk/custom-auth/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../enum/src/Core/JsonDeserializer.php | 30 +++- seed/php-sdk/enum/src/Core/JsonSerializer.php | 25 ++- .../enum/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/enum/src/Core/Union.php | 5 + seed/php-sdk/enum/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../enum/tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../enum/tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../enum/tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../enum/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../php-sdk/error-property/src/Core/Union.php | 5 + .../php-sdk/error-property/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../examples/src/Core/JsonDeserializer.php | 30 +++- .../examples/src/Core/JsonSerializer.php | 25 ++- .../examples/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/examples/src/Core/Union.php | 5 + seed/php-sdk/examples/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../examples/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../exhaustive/src/Core/JsonDeserializer.php | 30 +++- .../exhaustive/src/Core/JsonSerializer.php | 25 ++- .../exhaustive/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/exhaustive/src/Core/Union.php | 5 + seed/php-sdk/exhaustive/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../extends/src/Core/JsonDeserializer.php | 30 +++- .../extends/src/Core/JsonSerializer.php | 25 ++- .../extends/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/extends/src/Core/Union.php | 5 + seed/php-sdk/extends/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../extends/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../extra-properties/src/Core/Union.php | 5 + .../extra-properties/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../file-download/src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/file-download/src/Core/Union.php | 5 + seed/php-sdk/file-download/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../file-upload/src/Core/JsonDeserializer.php | 30 +++- .../file-upload/src/Core/JsonSerializer.php | 25 ++- .../file-upload/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/file-upload/src/Core/Union.php | 5 + seed/php-sdk/file-upload/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../folders/src/Core/JsonDeserializer.php | 30 +++- .../folders/src/Core/JsonSerializer.php | 25 ++- .../folders/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/folders/src/Core/Union.php | 5 + seed/php-sdk/folders/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../folders/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../grpc-proto-exhaustive/src/Core/Union.php | 5 + .../grpc-proto-exhaustive/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../grpc-proto/src/Core/JsonDeserializer.php | 30 +++- .../grpc-proto/src/Core/JsonSerializer.php | 25 ++- .../grpc-proto/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/grpc-proto/src/Core/Union.php | 5 + seed/php-sdk/grpc-proto/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../idempotency-headers/src/Core/Union.php | 5 + .../idempotency-headers/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../imdb/src/Core/JsonDeserializer.php | 30 +++- seed/php-sdk/imdb/src/Core/JsonSerializer.php | 25 ++- .../imdb/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/imdb/src/Core/Union.php | 5 + seed/php-sdk/imdb/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../imdb/tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../imdb/tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../imdb/tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../imdb/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../literal/src/Core/JsonDeserializer.php | 30 +++- .../literal/src/Core/JsonSerializer.php | 25 ++- .../literal/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/literal/src/Core/Union.php | 5 + seed/php-sdk/literal/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../literal/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../mixed-case/src/Core/JsonDeserializer.php | 30 +++- .../mixed-case/src/Core/JsonSerializer.php | 25 ++- .../mixed-case/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/mixed-case/src/Core/Union.php | 5 + seed/php-sdk/mixed-case/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../mixed-file-directory/src/Core/Union.php | 5 + .../mixed-file-directory/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../multi-line-docs/src/Core/Union.php | 5 + .../multi-line-docs/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../php-sdk/no-environment/src/Core/Union.php | 5 + .../php-sdk/no-environment/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../object/src/Core/JsonDeserializer.php | 30 +++- .../object/src/Core/JsonSerializer.php | 25 ++- .../object/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/object/src/Core/Union.php | 5 + seed/php-sdk/object/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../object/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../objects-with-imports/src/Core/Union.php | 5 + .../objects-with-imports/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../optional/src/Core/JsonDeserializer.php | 30 +++- .../optional/src/Core/JsonSerializer.php | 25 ++- .../optional/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/optional/src/Core/Union.php | 5 + seed/php-sdk/optional/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../optional/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../package-yml/src/Core/JsonDeserializer.php | 30 +++- .../package-yml/src/Core/JsonSerializer.php | 25 ++- .../package-yml/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/package-yml/src/Core/Union.php | 5 + seed/php-sdk/package-yml/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../pagination/src/Core/JsonDeserializer.php | 30 +++- .../pagination/src/Core/JsonSerializer.php | 25 ++- .../pagination/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/pagination/src/Core/Union.php | 5 + seed/php-sdk/pagination/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../plain-text/src/Core/JsonDeserializer.php | 30 +++- .../plain-text/src/Core/JsonSerializer.php | 25 ++- .../plain-text/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/plain-text/src/Core/Union.php | 5 + seed/php-sdk/plain-text/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../query-parameters/src/Core/Union.php | 5 + .../query-parameters/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../reserved-keywords/src/Core/Union.php | 5 + .../reserved-keywords/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../response-property/src/Core/Union.php | 5 + .../response-property/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../simple-fhir/src/Core/JsonDeserializer.php | 30 +++- .../simple-fhir/src/Core/JsonSerializer.php | 25 ++- .../simple-fhir/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/simple-fhir/src/Core/Union.php | 5 + seed/php-sdk/simple-fhir/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../src/Core/Union.php | 5 + .../src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../streaming-parameter/src/Core/Union.php | 5 + .../streaming-parameter/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../streaming/src/Core/JsonDeserializer.php | 30 +++- .../streaming/src/Core/JsonSerializer.php | 25 ++- .../streaming/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/streaming/src/Core/Union.php | 5 + seed/php-sdk/streaming/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../trace/src/Core/JsonDeserializer.php | 30 +++- .../php-sdk/trace/src/Core/JsonSerializer.php | 25 ++- .../trace/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/trace/src/Core/Union.php | 5 + seed/php-sdk/trace/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../trace/tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../trace/tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../trace/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../undiscriminated-unions/src/Core/Union.php | 5 + .../undiscriminated-unions/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../unions/src/Core/JsonDeserializer.php | 30 +++- .../unions/src/Core/JsonSerializer.php | 25 ++- .../unions/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/unions/src/Core/Union.php | 5 + seed/php-sdk/unions/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../unions/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../unknown/src/Core/JsonDeserializer.php | 30 +++- .../unknown/src/Core/JsonSerializer.php | 25 ++- .../unknown/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/unknown/src/Core/Union.php | 5 + seed/php-sdk/unknown/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../unknown/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../validation/src/Core/JsonDeserializer.php | 30 +++- .../validation/src/Core/JsonSerializer.php | 25 ++- .../validation/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/validation/src/Core/Union.php | 5 + seed/php-sdk/validation/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../variables/src/Core/JsonDeserializer.php | 30 +++- .../variables/src/Core/JsonSerializer.php | 25 ++- .../variables/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/variables/src/Core/Union.php | 5 + seed/php-sdk/variables/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../src/Core/JsonDeserializer.php | 30 +++- .../src/Core/JsonSerializer.php | 25 ++- .../src/Core/SerializableType.php | 80 +++++---- .../version-no-default/src/Core/Union.php | 5 + .../version-no-default/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../version/src/Core/JsonDeserializer.php | 30 +++- .../version/src/Core/JsonSerializer.php | 25 ++- .../version/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/version/src/Core/Union.php | 5 + seed/php-sdk/version/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../version/tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- .../websocket/src/Core/JsonDeserializer.php | 30 +++- .../websocket/src/Core/JsonSerializer.php | 25 ++- .../websocket/src/Core/SerializableType.php | 80 +++++---- seed/php-sdk/websocket/src/Core/Union.php | 5 + seed/php-sdk/websocket/src/Core/Utils.php | 19 ++ .../tests/Seed/Core/DateArrayTypeTest.php | 17 +- .../tests/Seed/Core/EmptyArraysTest.php | 42 +++-- .../tests/Seed/Core/InvalidTypesTest.php | 15 +- .../Seed/Core/MixedDateArrayTypeTest.php | 17 +- .../Seed/Core/NestedUnionArrayTypeTest.php | 31 +++- .../tests/Seed/Core/NullPropertyTypeTest.php | 27 ++- .../tests/Seed/Core/NullableArrayTypeTest.php | 16 +- .../tests/Seed/Core/ScalarTypesTest.php | 67 +++++-- .../tests/Seed/Core/TestTypeTest.php | 154 +++++++++++----- .../tests/Seed/Core/UnionArrayTypeTest.php | 17 +- 1815 files changed, 50239 insertions(+), 17830 deletions(-) diff --git a/generators/php/codegen/src/asIs/DateArrayTypeTest.Template.php b/generators/php/codegen/src/asIs/DateArrayTypeTest.Template.php index 2403b584696..ca53121351f 100644 --- a/generators/php/codegen/src/asIs/DateArrayTypeTest.Template.php +++ b/generators/php/codegen/src/asIs/DateArrayTypeTest.Template.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/generators/php/codegen/src/asIs/EmptyArraysTest.Template.php b/generators/php/codegen/src/asIs/EmptyArraysTest.Template.php index d104b46cdc7..467da550246 100644 --- a/generators/php/codegen/src/asIs/EmptyArraysTest.Template.php +++ b/generators/php/codegen/src/asIs/EmptyArraysTest.Template.php @@ -11,24 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray - ) - { + array $values, + ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } @@ -55,4 +70,4 @@ public function testEmptyArrays(): void $this->assertEmpty($object->emptyMapArray, 'empty_map_array should be empty.'); $this->assertEmpty($object->emptyDatesArray, 'empty_dates_array should be empty.'); } -} \ No newline at end of file +} diff --git a/generators/php/codegen/src/asIs/InvalidTypesTest.Template.php b/generators/php/codegen/src/asIs/InvalidTypesTest.Template.php index 1b2c3ac7a50..6c05c3a4bf6 100644 --- a/generators/php/codegen/src/asIs/InvalidTypesTest.Template.php +++ b/generators/php/codegen/src/asIs/InvalidTypesTest.Template.php @@ -8,11 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty - ) - { + array $values, + ) { + $this->integerProperty = $values['integerProperty']; } } @@ -32,4 +42,4 @@ public function testInvalidTypesThrowExceptions(): void // Attempt to deserialize invalid data InvalidType::fromJson($json); } -} \ No newline at end of file +} diff --git a/generators/php/codegen/src/asIs/JsonDeserializer.Template.php b/generators/php/codegen/src/asIs/JsonDeserializer.Template.php index bdec6f8b9af..a9d5c2702c8 100644 --- a/generators/php/codegen/src/asIs/JsonDeserializer.Template.php +++ b/generators/php/codegen/src/asIs/JsonDeserializer.Template.php @@ -1,6 +1,6 @@ ; +namespace <%= coreNamespace%>; use DateTime; use Exception; @@ -69,11 +69,13 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +114,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +124,23 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * @@ -158,6 +174,6 @@ private static function deserializeMap(array $data, array $type): array private static function deserializeList(array $data, array $type): array { $valueType = $type[0]; - return array_map(fn($item) => self::deserializeValue($item, $valueType), $data); + return array_map(fn ($item) => self::deserializeValue($item, $valueType), $data); } -} \ No newline at end of file +} diff --git a/generators/php/codegen/src/asIs/JsonSerializer.Template.php b/generators/php/codegen/src/asIs/JsonSerializer.Template.php index 15cb013fb44..e1d8282bacb 100644 --- a/generators/php/codegen/src/asIs/JsonSerializer.Template.php +++ b/generators/php/codegen/src/asIs/JsonSerializer.Template.php @@ -1,6 +1,6 @@ ; +namespace <%= coreNamespace%>; use DateTime; use Exception; @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,21 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * @@ -150,6 +162,6 @@ private static function serializeMap(array $data, array $type): array private static function serializeList(array $data, array $type): array { $valueType = $type[0]; - return array_map(fn($item) => self::serializeValue($item, $valueType), $data); + return array_map(fn ($item) => self::serializeValue($item, $valueType), $data); } -} \ No newline at end of file +} diff --git a/generators/php/codegen/src/asIs/MixedDateArrayTypeTest.Template.php b/generators/php/codegen/src/asIs/MixedDateArrayTypeTest.Template.php index 0d3cf2b875e..cf0dc64a1fb 100644 --- a/generators/php/codegen/src/asIs/MixedDateArrayTypeTest.Template.php +++ b/generators/php/codegen/src/asIs/MixedDateArrayTypeTest.Template.php @@ -11,12 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates - ) - { + array $values, + ) { + $this->mixedDates = $values['mixedDates']; } } @@ -47,4 +57,4 @@ public function testDateTimeTypesInUnionArrays(): void $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for mixed_dates.'); } -} \ No newline at end of file +} diff --git a/generators/php/codegen/src/asIs/NestedUnionArrayTypeTest.Template.php b/generators/php/codegen/src/asIs/NestedUnionArrayTypeTest.Template.php index b1e6198fce3..b5b5016976d 100644 --- a/generators/php/codegen/src/asIs/NestedUnionArrayTypeTest.Template.php +++ b/generators/php/codegen/src/asIs/NestedUnionArrayTypeTest.Template.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/generators/php/codegen/src/asIs/NullPropertyTypeTest.Template.php b/generators/php/codegen/src/asIs/NullPropertyTypeTest.Template.php index 36738897617..46ff130f7ee 100644 --- a/generators/php/codegen/src/asIs/NullPropertyTypeTest.Template.php +++ b/generators/php/codegen/src/asIs/NullPropertyTypeTest.Template.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/generators/php/codegen/src/asIs/NullableArrayTypeTest.Template.php b/generators/php/codegen/src/asIs/NullableArrayTypeTest.Template.php index 6aaf487ead6..f1a2d60963c 100644 --- a/generators/php/codegen/src/asIs/NullableArrayTypeTest.Template.php +++ b/generators/php/codegen/src/asIs/NullableArrayTypeTest.Template.php @@ -11,14 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray - ) - { + array $values, + ) { + $this->nullableStringArray = $values['nullableStringArray']; } } @@ -40,4 +47,4 @@ public function testNullableTypesInArrays(): void $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for nullable_string_array.'); } -} \ No newline at end of file +} diff --git a/generators/php/codegen/src/asIs/ScalarTypesTest.Template.php b/generators/php/codegen/src/asIs/ScalarTypesTest.Template.php index 1330506cad6..3728b7ccc5f 100644 --- a/generators/php/codegen/src/asIs/ScalarTypesTest.Template.php +++ b/generators/php/codegen/src/asIs/ScalarTypesTest.Template.php @@ -10,27 +10,62 @@ class ScalarTypesTest extends SerializableType { - public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; - #[JsonProperty('float_property')] - public float $floatProperty, + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; - #[JsonProperty('boolean_property')] - public bool $booleanProperty, + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; - #[JsonProperty('string_property')] - public string $stringProperty, + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null - ) - { + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ + public function __construct( + array $values, + ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } @@ -66,4 +101,4 @@ public function testAllScalarTypesIncludingFloat(): void // Check int_float_array $this->assertEquals([1, 2.5, 3, 4.75], $object->intFloatArray, 'int_float_array should match the original data.'); } -} \ No newline at end of file +} diff --git a/generators/php/codegen/src/asIs/SerializableType.Template.php b/generators/php/codegen/src/asIs/SerializableType.Template.php index cb9aa14d7b7..39acdc2b8ca 100644 --- a/generators/php/codegen/src/asIs/SerializableType.Template.php +++ b/generators/php/codegen/src/asIs/SerializableType.Template.php @@ -1,9 +1,11 @@ ; +namespace <%= coreNamespace%>; use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,9 +14,13 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ - public function toJson(): string { + public function toJson(): string + { $serializedObject = $this->jsonSerialize(); $encoded = JsonEncoder::encode($serializedObject); if (!$encoded) { @@ -24,8 +30,10 @@ public function toJson(): string { } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -55,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -64,44 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ - public static function fromJson(string $json): static { + public static function fromJson(string $json): static + { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -110,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -124,24 +135,31 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { $jsonPropertyAttr = $property->getAttributes(JsonProperty::class)[0] ?? null; return $jsonPropertyAttr?->newInstance()?->name; } -} +} \ No newline at end of file diff --git a/generators/php/codegen/src/asIs/TestTypeTest.Template.php b/generators/php/codegen/src/asIs/TestTypeTest.Template.php index 88a79543880..659a70973c9 100644 --- a/generators/php/codegen/src/asIs/TestTypeTest.Template.php +++ b/generators/php/codegen/src/asIs/TestTypeTest.Template.php @@ -13,67 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty - ) - { + array $values, + ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end - ) - { + array $values, + ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -86,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -93,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -129,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings @@ -145,4 +198,4 @@ public function testSerializationAndDeserialization(): void $this->assertInstanceOf(DateTime::class, $object->datesArray[2], 'dates_array[2] should be a DateTime instance.'); $this->assertEquals('2023-03-01', $object->datesArray[2]->format('Y-m-d'), 'dates_array[2] should have the correct date.'); } -} \ No newline at end of file +} diff --git a/generators/php/codegen/src/asIs/Union.Template.php b/generators/php/codegen/src/asIs/Union.Template.php index e353dd63a8e..891e1788f98 100644 --- a/generators/php/codegen/src/asIs/Union.Template.php +++ b/generators/php/codegen/src/asIs/Union.Template.php @@ -1,6 +1,6 @@ ; +namespace <%= coreNamespace%>; class Union { @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } -} \ No newline at end of file + + public function __toString(): string { + return implode(' | ', $this->types); + } +} + \ No newline at end of file diff --git a/generators/php/codegen/src/asIs/UnionArrayTypeTest.Template.php b/generators/php/codegen/src/asIs/UnionArrayTypeTest.Template.php index 412fdae524a..95ada8b638d 100644 --- a/generators/php/codegen/src/asIs/UnionArrayTypeTest.Template.php +++ b/generators/php/codegen/src/asIs/UnionArrayTypeTest.Template.php @@ -11,15 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray - ) - { + array $values, + ) { + $this->mixedArray = $values['mixedArray']; } } @@ -47,4 +53,4 @@ public function testUnionTypesInArrays(): void $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for mixed_array.'); } -} \ No newline at end of file +} diff --git a/generators/php/codegen/src/asIs/Utils.Template.php b/generators/php/codegen/src/asIs/Utils.Template.php index df914d9cc5f..243b4286952 100644 --- a/generators/php/codegen/src/asIs/Utils.Template.php +++ b/generators/php/codegen/src/asIs/Utils.Template.php @@ -1,6 +1,6 @@ ; +namespace <%= coreNamespace%>; use DateTime; use Exception; @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } -} \ No newline at end of file + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } +} diff --git a/seed/php-model/alias-extends/src/Core/JsonDeserializer.php b/seed/php-model/alias-extends/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/alias-extends/src/Core/JsonDeserializer.php +++ b/seed/php-model/alias-extends/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/alias-extends/src/Core/JsonSerializer.php b/seed/php-model/alias-extends/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/alias-extends/src/Core/JsonSerializer.php +++ b/seed/php-model/alias-extends/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/alias-extends/src/Core/SerializableType.php b/seed/php-model/alias-extends/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/alias-extends/src/Core/SerializableType.php +++ b/seed/php-model/alias-extends/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/alias-extends/src/Core/Union.php b/seed/php-model/alias-extends/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/alias-extends/src/Core/Union.php +++ b/seed/php-model/alias-extends/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/alias-extends/src/Core/Utils.php b/seed/php-model/alias-extends/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/alias-extends/src/Core/Utils.php +++ b/seed/php-model/alias-extends/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/alias-extends/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/alias-extends/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/alias-extends/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/alias-extends/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/alias-extends/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/alias-extends/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/alias-extends/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/alias-extends/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/alias-extends/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/alias-extends/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/alias-extends/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/alias-extends/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/alias-extends/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/alias-extends/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/alias-extends/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/alias-extends/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/alias-extends/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/alias-extends/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/alias-extends/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/alias-extends/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/alias-extends/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/alias-extends/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/alias-extends/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/alias-extends/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/alias-extends/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/alias-extends/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/alias-extends/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/alias-extends/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/alias-extends/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/alias-extends/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/alias-extends/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/alias-extends/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/alias-extends/tests/Seed/Core/TestTypeTest.php b/seed/php-model/alias-extends/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/alias-extends/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/alias-extends/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/alias-extends/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/alias-extends/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/alias-extends/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/alias-extends/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/alias/src/Core/JsonDeserializer.php b/seed/php-model/alias/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/alias/src/Core/JsonDeserializer.php +++ b/seed/php-model/alias/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/alias/src/Core/JsonSerializer.php b/seed/php-model/alias/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/alias/src/Core/JsonSerializer.php +++ b/seed/php-model/alias/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/alias/src/Core/SerializableType.php b/seed/php-model/alias/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/alias/src/Core/SerializableType.php +++ b/seed/php-model/alias/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/alias/src/Core/Union.php b/seed/php-model/alias/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/alias/src/Core/Union.php +++ b/seed/php-model/alias/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/alias/src/Core/Utils.php b/seed/php-model/alias/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/alias/src/Core/Utils.php +++ b/seed/php-model/alias/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/alias/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/alias/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/alias/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/alias/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/alias/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/alias/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/alias/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/alias/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/alias/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/alias/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/alias/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/alias/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/alias/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/alias/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/alias/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/alias/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/alias/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/alias/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/alias/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/alias/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/alias/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/alias/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/alias/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/alias/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/alias/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/alias/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/alias/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/alias/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/alias/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/alias/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/alias/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/alias/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/alias/tests/Seed/Core/TestTypeTest.php b/seed/php-model/alias/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/alias/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/alias/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/alias/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/alias/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/alias/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/alias/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/any-auth/src/Core/JsonDeserializer.php b/seed/php-model/any-auth/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/any-auth/src/Core/JsonDeserializer.php +++ b/seed/php-model/any-auth/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/any-auth/src/Core/JsonSerializer.php b/seed/php-model/any-auth/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/any-auth/src/Core/JsonSerializer.php +++ b/seed/php-model/any-auth/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/any-auth/src/Core/SerializableType.php b/seed/php-model/any-auth/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/any-auth/src/Core/SerializableType.php +++ b/seed/php-model/any-auth/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/any-auth/src/Core/Union.php b/seed/php-model/any-auth/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/any-auth/src/Core/Union.php +++ b/seed/php-model/any-auth/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/any-auth/src/Core/Utils.php b/seed/php-model/any-auth/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/any-auth/src/Core/Utils.php +++ b/seed/php-model/any-auth/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/any-auth/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/any-auth/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/any-auth/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/any-auth/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/any-auth/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/any-auth/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/any-auth/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/any-auth/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/any-auth/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/any-auth/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/any-auth/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/any-auth/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/any-auth/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/any-auth/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/any-auth/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/any-auth/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/any-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/any-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/any-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/any-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/any-auth/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/any-auth/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/any-auth/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/any-auth/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/any-auth/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/any-auth/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/any-auth/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/any-auth/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/any-auth/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/any-auth/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/any-auth/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/any-auth/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/any-auth/tests/Seed/Core/TestTypeTest.php b/seed/php-model/any-auth/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/any-auth/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/any-auth/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/any-auth/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/any-auth/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/any-auth/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/any-auth/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/api-wide-base-path/src/Core/JsonDeserializer.php b/seed/php-model/api-wide-base-path/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/api-wide-base-path/src/Core/JsonDeserializer.php +++ b/seed/php-model/api-wide-base-path/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/api-wide-base-path/src/Core/JsonSerializer.php b/seed/php-model/api-wide-base-path/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/api-wide-base-path/src/Core/JsonSerializer.php +++ b/seed/php-model/api-wide-base-path/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/api-wide-base-path/src/Core/SerializableType.php b/seed/php-model/api-wide-base-path/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/api-wide-base-path/src/Core/SerializableType.php +++ b/seed/php-model/api-wide-base-path/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/api-wide-base-path/src/Core/Union.php b/seed/php-model/api-wide-base-path/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/api-wide-base-path/src/Core/Union.php +++ b/seed/php-model/api-wide-base-path/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/api-wide-base-path/src/Core/Utils.php b/seed/php-model/api-wide-base-path/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/api-wide-base-path/src/Core/Utils.php +++ b/seed/php-model/api-wide-base-path/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/api-wide-base-path/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/api-wide-base-path/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/api-wide-base-path/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/api-wide-base-path/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/api-wide-base-path/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/api-wide-base-path/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/api-wide-base-path/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/api-wide-base-path/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/TestTypeTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/api-wide-base-path/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/api-wide-base-path/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/audiences/src/Core/JsonDeserializer.php b/seed/php-model/audiences/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/audiences/src/Core/JsonDeserializer.php +++ b/seed/php-model/audiences/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/audiences/src/Core/JsonSerializer.php b/seed/php-model/audiences/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/audiences/src/Core/JsonSerializer.php +++ b/seed/php-model/audiences/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/audiences/src/Core/SerializableType.php b/seed/php-model/audiences/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/audiences/src/Core/SerializableType.php +++ b/seed/php-model/audiences/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/audiences/src/Core/Union.php b/seed/php-model/audiences/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/audiences/src/Core/Union.php +++ b/seed/php-model/audiences/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/audiences/src/Core/Utils.php b/seed/php-model/audiences/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/audiences/src/Core/Utils.php +++ b/seed/php-model/audiences/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/audiences/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/audiences/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/audiences/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/audiences/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/audiences/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/audiences/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/audiences/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/audiences/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/audiences/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/audiences/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/audiences/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/audiences/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/audiences/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/audiences/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/audiences/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/audiences/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/audiences/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/audiences/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/audiences/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/audiences/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/audiences/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/audiences/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/audiences/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/audiences/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/audiences/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/audiences/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/audiences/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/audiences/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/audiences/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/audiences/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/audiences/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/audiences/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/audiences/tests/Seed/Core/TestTypeTest.php b/seed/php-model/audiences/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/audiences/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/audiences/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/audiences/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/audiences/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/audiences/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/audiences/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/auth-environment-variables/src/Core/JsonDeserializer.php b/seed/php-model/auth-environment-variables/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/auth-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-model/auth-environment-variables/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/auth-environment-variables/src/Core/JsonSerializer.php b/seed/php-model/auth-environment-variables/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/auth-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-model/auth-environment-variables/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/auth-environment-variables/src/Core/SerializableType.php b/seed/php-model/auth-environment-variables/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/auth-environment-variables/src/Core/SerializableType.php +++ b/seed/php-model/auth-environment-variables/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/auth-environment-variables/src/Core/Union.php b/seed/php-model/auth-environment-variables/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/auth-environment-variables/src/Core/Union.php +++ b/seed/php-model/auth-environment-variables/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/auth-environment-variables/src/Core/Utils.php b/seed/php-model/auth-environment-variables/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/auth-environment-variables/src/Core/Utils.php +++ b/seed/php-model/auth-environment-variables/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/TestTypeTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/auth-environment-variables/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/basic-auth-environment-variables/src/Core/JsonDeserializer.php b/seed/php-model/basic-auth-environment-variables/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/basic-auth-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-model/basic-auth-environment-variables/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/basic-auth-environment-variables/src/Core/JsonSerializer.php b/seed/php-model/basic-auth-environment-variables/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/basic-auth-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-model/basic-auth-environment-variables/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/basic-auth-environment-variables/src/Core/SerializableType.php b/seed/php-model/basic-auth-environment-variables/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/basic-auth-environment-variables/src/Core/SerializableType.php +++ b/seed/php-model/basic-auth-environment-variables/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/basic-auth-environment-variables/src/Core/Union.php b/seed/php-model/basic-auth-environment-variables/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/basic-auth-environment-variables/src/Core/Union.php +++ b/seed/php-model/basic-auth-environment-variables/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/basic-auth-environment-variables/src/Core/Utils.php b/seed/php-model/basic-auth-environment-variables/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/basic-auth-environment-variables/src/Core/Utils.php +++ b/seed/php-model/basic-auth-environment-variables/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/TestTypeTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/basic-auth/src/Core/JsonDeserializer.php b/seed/php-model/basic-auth/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/basic-auth/src/Core/JsonDeserializer.php +++ b/seed/php-model/basic-auth/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/basic-auth/src/Core/JsonSerializer.php b/seed/php-model/basic-auth/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/basic-auth/src/Core/JsonSerializer.php +++ b/seed/php-model/basic-auth/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/basic-auth/src/Core/SerializableType.php b/seed/php-model/basic-auth/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/basic-auth/src/Core/SerializableType.php +++ b/seed/php-model/basic-auth/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/basic-auth/src/Core/Union.php b/seed/php-model/basic-auth/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/basic-auth/src/Core/Union.php +++ b/seed/php-model/basic-auth/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/basic-auth/src/Core/Utils.php b/seed/php-model/basic-auth/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/basic-auth/src/Core/Utils.php +++ b/seed/php-model/basic-auth/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/basic-auth/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/basic-auth/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/basic-auth/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/basic-auth/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/basic-auth/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/basic-auth/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/basic-auth/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/basic-auth/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/basic-auth/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/basic-auth/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/basic-auth/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/basic-auth/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/basic-auth/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/basic-auth/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/basic-auth/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/basic-auth/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/basic-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/basic-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/basic-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/basic-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/basic-auth/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/basic-auth/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/basic-auth/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/basic-auth/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/basic-auth/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/basic-auth/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/basic-auth/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/basic-auth/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/basic-auth/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/basic-auth/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/basic-auth/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/basic-auth/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/basic-auth/tests/Seed/Core/TestTypeTest.php b/seed/php-model/basic-auth/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/basic-auth/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/basic-auth/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/basic-auth/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/basic-auth/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/basic-auth/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/basic-auth/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/bearer-token-environment-variable/src/Core/JsonDeserializer.php b/seed/php-model/bearer-token-environment-variable/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/bearer-token-environment-variable/src/Core/JsonDeserializer.php +++ b/seed/php-model/bearer-token-environment-variable/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/bearer-token-environment-variable/src/Core/JsonSerializer.php b/seed/php-model/bearer-token-environment-variable/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/bearer-token-environment-variable/src/Core/JsonSerializer.php +++ b/seed/php-model/bearer-token-environment-variable/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/bearer-token-environment-variable/src/Core/SerializableType.php b/seed/php-model/bearer-token-environment-variable/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/bearer-token-environment-variable/src/Core/SerializableType.php +++ b/seed/php-model/bearer-token-environment-variable/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/bearer-token-environment-variable/src/Core/Union.php b/seed/php-model/bearer-token-environment-variable/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/bearer-token-environment-variable/src/Core/Union.php +++ b/seed/php-model/bearer-token-environment-variable/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/bearer-token-environment-variable/src/Core/Utils.php b/seed/php-model/bearer-token-environment-variable/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/bearer-token-environment-variable/src/Core/Utils.php +++ b/seed/php-model/bearer-token-environment-variable/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/TestTypeTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/bytes/src/Core/JsonDeserializer.php b/seed/php-model/bytes/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/bytes/src/Core/JsonDeserializer.php +++ b/seed/php-model/bytes/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/bytes/src/Core/JsonSerializer.php b/seed/php-model/bytes/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/bytes/src/Core/JsonSerializer.php +++ b/seed/php-model/bytes/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/bytes/src/Core/SerializableType.php b/seed/php-model/bytes/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/bytes/src/Core/SerializableType.php +++ b/seed/php-model/bytes/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/bytes/src/Core/Union.php b/seed/php-model/bytes/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/bytes/src/Core/Union.php +++ b/seed/php-model/bytes/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/bytes/src/Core/Utils.php b/seed/php-model/bytes/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/bytes/src/Core/Utils.php +++ b/seed/php-model/bytes/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/bytes/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/bytes/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/bytes/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/bytes/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/bytes/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/bytes/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/bytes/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/bytes/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/bytes/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/bytes/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/bytes/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/bytes/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/bytes/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/bytes/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/bytes/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/bytes/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/bytes/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/bytes/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/bytes/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/bytes/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/bytes/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/bytes/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/bytes/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/bytes/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/bytes/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/bytes/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/bytes/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/bytes/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/bytes/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/bytes/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/bytes/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/bytes/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/bytes/tests/Seed/Core/TestTypeTest.php b/seed/php-model/bytes/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/bytes/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/bytes/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/bytes/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/bytes/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/bytes/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/bytes/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/circular-references-advanced/src/Core/JsonDeserializer.php b/seed/php-model/circular-references-advanced/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/circular-references-advanced/src/Core/JsonDeserializer.php +++ b/seed/php-model/circular-references-advanced/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/circular-references-advanced/src/Core/JsonSerializer.php b/seed/php-model/circular-references-advanced/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/circular-references-advanced/src/Core/JsonSerializer.php +++ b/seed/php-model/circular-references-advanced/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/circular-references-advanced/src/Core/SerializableType.php b/seed/php-model/circular-references-advanced/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/circular-references-advanced/src/Core/SerializableType.php +++ b/seed/php-model/circular-references-advanced/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/circular-references-advanced/src/Core/Union.php b/seed/php-model/circular-references-advanced/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/circular-references-advanced/src/Core/Union.php +++ b/seed/php-model/circular-references-advanced/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/circular-references-advanced/src/Core/Utils.php b/seed/php-model/circular-references-advanced/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/circular-references-advanced/src/Core/Utils.php +++ b/seed/php-model/circular-references-advanced/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/circular-references-advanced/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/circular-references-advanced/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/circular-references-advanced/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/circular-references-advanced/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/circular-references-advanced/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/circular-references-advanced/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/circular-references-advanced/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/circular-references-advanced/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/TestTypeTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/circular-references-advanced/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/circular-references-advanced/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/circular-references/src/Core/JsonDeserializer.php b/seed/php-model/circular-references/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/circular-references/src/Core/JsonDeserializer.php +++ b/seed/php-model/circular-references/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/circular-references/src/Core/JsonSerializer.php b/seed/php-model/circular-references/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/circular-references/src/Core/JsonSerializer.php +++ b/seed/php-model/circular-references/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/circular-references/src/Core/SerializableType.php b/seed/php-model/circular-references/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/circular-references/src/Core/SerializableType.php +++ b/seed/php-model/circular-references/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/circular-references/src/Core/Union.php b/seed/php-model/circular-references/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/circular-references/src/Core/Union.php +++ b/seed/php-model/circular-references/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/circular-references/src/Core/Utils.php b/seed/php-model/circular-references/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/circular-references/src/Core/Utils.php +++ b/seed/php-model/circular-references/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/circular-references/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/circular-references/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/circular-references/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/circular-references/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/circular-references/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/circular-references/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/circular-references/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/circular-references/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/circular-references/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/circular-references/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/circular-references/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/circular-references/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/circular-references/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/circular-references/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/circular-references/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/circular-references/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/circular-references/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/circular-references/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/circular-references/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/circular-references/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/circular-references/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/circular-references/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/circular-references/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/circular-references/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/circular-references/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/circular-references/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/circular-references/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/circular-references/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/circular-references/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/circular-references/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/circular-references/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/circular-references/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/circular-references/tests/Seed/Core/TestTypeTest.php b/seed/php-model/circular-references/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/circular-references/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/circular-references/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/circular-references/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/circular-references/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/circular-references/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/circular-references/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/cross-package-type-names/src/Core/JsonDeserializer.php b/seed/php-model/cross-package-type-names/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/cross-package-type-names/src/Core/JsonDeserializer.php +++ b/seed/php-model/cross-package-type-names/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/cross-package-type-names/src/Core/JsonSerializer.php b/seed/php-model/cross-package-type-names/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/cross-package-type-names/src/Core/JsonSerializer.php +++ b/seed/php-model/cross-package-type-names/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/cross-package-type-names/src/Core/SerializableType.php b/seed/php-model/cross-package-type-names/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/cross-package-type-names/src/Core/SerializableType.php +++ b/seed/php-model/cross-package-type-names/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/cross-package-type-names/src/Core/Union.php b/seed/php-model/cross-package-type-names/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/cross-package-type-names/src/Core/Union.php +++ b/seed/php-model/cross-package-type-names/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/cross-package-type-names/src/Core/Utils.php b/seed/php-model/cross-package-type-names/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/cross-package-type-names/src/Core/Utils.php +++ b/seed/php-model/cross-package-type-names/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/cross-package-type-names/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/cross-package-type-names/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/cross-package-type-names/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/cross-package-type-names/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/cross-package-type-names/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/cross-package-type-names/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/cross-package-type-names/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/cross-package-type-names/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/TestTypeTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/cross-package-type-names/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/cross-package-type-names/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/custom-auth/src/Core/JsonDeserializer.php b/seed/php-model/custom-auth/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/custom-auth/src/Core/JsonDeserializer.php +++ b/seed/php-model/custom-auth/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/custom-auth/src/Core/JsonSerializer.php b/seed/php-model/custom-auth/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/custom-auth/src/Core/JsonSerializer.php +++ b/seed/php-model/custom-auth/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/custom-auth/src/Core/SerializableType.php b/seed/php-model/custom-auth/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/custom-auth/src/Core/SerializableType.php +++ b/seed/php-model/custom-auth/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/custom-auth/src/Core/Union.php b/seed/php-model/custom-auth/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/custom-auth/src/Core/Union.php +++ b/seed/php-model/custom-auth/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/custom-auth/src/Core/Utils.php b/seed/php-model/custom-auth/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/custom-auth/src/Core/Utils.php +++ b/seed/php-model/custom-auth/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/custom-auth/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/custom-auth/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/custom-auth/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/custom-auth/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/custom-auth/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/custom-auth/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/custom-auth/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/custom-auth/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/custom-auth/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/custom-auth/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/custom-auth/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/custom-auth/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/custom-auth/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/custom-auth/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/custom-auth/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/custom-auth/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/custom-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/custom-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/custom-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/custom-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/custom-auth/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/custom-auth/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/custom-auth/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/custom-auth/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/custom-auth/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/custom-auth/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/custom-auth/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/custom-auth/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/custom-auth/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/custom-auth/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/custom-auth/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/custom-auth/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/custom-auth/tests/Seed/Core/TestTypeTest.php b/seed/php-model/custom-auth/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/custom-auth/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/custom-auth/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/custom-auth/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/custom-auth/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/custom-auth/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/custom-auth/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/enum/src/Core/JsonDeserializer.php b/seed/php-model/enum/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/enum/src/Core/JsonDeserializer.php +++ b/seed/php-model/enum/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/enum/src/Core/JsonSerializer.php b/seed/php-model/enum/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/enum/src/Core/JsonSerializer.php +++ b/seed/php-model/enum/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/enum/src/Core/SerializableType.php b/seed/php-model/enum/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/enum/src/Core/SerializableType.php +++ b/seed/php-model/enum/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/enum/src/Core/Union.php b/seed/php-model/enum/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/enum/src/Core/Union.php +++ b/seed/php-model/enum/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/enum/src/Core/Utils.php b/seed/php-model/enum/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/enum/src/Core/Utils.php +++ b/seed/php-model/enum/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/enum/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/enum/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/enum/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/enum/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/enum/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/enum/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/enum/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/enum/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/enum/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/enum/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/enum/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/enum/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/enum/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/enum/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/enum/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/enum/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/enum/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/enum/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/enum/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/enum/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/enum/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/enum/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/enum/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/enum/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/enum/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/enum/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/enum/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/enum/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/enum/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/enum/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/enum/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/enum/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/enum/tests/Seed/Core/TestTypeTest.php b/seed/php-model/enum/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/enum/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/enum/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/enum/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/enum/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/enum/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/enum/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/error-property/src/Core/JsonDeserializer.php b/seed/php-model/error-property/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/error-property/src/Core/JsonDeserializer.php +++ b/seed/php-model/error-property/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/error-property/src/Core/JsonSerializer.php b/seed/php-model/error-property/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/error-property/src/Core/JsonSerializer.php +++ b/seed/php-model/error-property/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/error-property/src/Core/SerializableType.php b/seed/php-model/error-property/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/error-property/src/Core/SerializableType.php +++ b/seed/php-model/error-property/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/error-property/src/Core/Union.php b/seed/php-model/error-property/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/error-property/src/Core/Union.php +++ b/seed/php-model/error-property/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/error-property/src/Core/Utils.php b/seed/php-model/error-property/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/error-property/src/Core/Utils.php +++ b/seed/php-model/error-property/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/error-property/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/error-property/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/error-property/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/error-property/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/error-property/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/error-property/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/error-property/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/error-property/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/error-property/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/error-property/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/error-property/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/error-property/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/error-property/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/error-property/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/error-property/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/error-property/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/error-property/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/error-property/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/error-property/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/error-property/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/error-property/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/error-property/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/error-property/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/error-property/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/error-property/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/error-property/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/error-property/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/error-property/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/error-property/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/error-property/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/error-property/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/error-property/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/error-property/tests/Seed/Core/TestTypeTest.php b/seed/php-model/error-property/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/error-property/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/error-property/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/error-property/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/error-property/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/error-property/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/error-property/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/examples/src/Core/JsonDeserializer.php b/seed/php-model/examples/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/examples/src/Core/JsonDeserializer.php +++ b/seed/php-model/examples/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/examples/src/Core/JsonSerializer.php b/seed/php-model/examples/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/examples/src/Core/JsonSerializer.php +++ b/seed/php-model/examples/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/examples/src/Core/SerializableType.php b/seed/php-model/examples/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/examples/src/Core/SerializableType.php +++ b/seed/php-model/examples/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/examples/src/Core/Union.php b/seed/php-model/examples/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/examples/src/Core/Union.php +++ b/seed/php-model/examples/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/examples/src/Core/Utils.php b/seed/php-model/examples/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/examples/src/Core/Utils.php +++ b/seed/php-model/examples/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/examples/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/examples/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/examples/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/examples/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/examples/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/examples/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/examples/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/examples/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/examples/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/examples/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/examples/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/examples/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/examples/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/examples/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/examples/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/examples/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/examples/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/examples/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/examples/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/examples/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/examples/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/examples/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/examples/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/examples/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/examples/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/examples/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/examples/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/examples/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/examples/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/examples/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/examples/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/examples/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/examples/tests/Seed/Core/TestTypeTest.php b/seed/php-model/examples/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/examples/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/examples/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/examples/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/examples/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/examples/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/examples/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/exhaustive/src/Core/JsonDeserializer.php b/seed/php-model/exhaustive/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-model/exhaustive/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/exhaustive/src/Core/JsonSerializer.php b/seed/php-model/exhaustive/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-model/exhaustive/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/exhaustive/src/Core/SerializableType.php b/seed/php-model/exhaustive/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/exhaustive/src/Core/SerializableType.php +++ b/seed/php-model/exhaustive/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/exhaustive/src/Core/Union.php b/seed/php-model/exhaustive/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/exhaustive/src/Core/Union.php +++ b/seed/php-model/exhaustive/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/exhaustive/src/Core/Utils.php b/seed/php-model/exhaustive/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/exhaustive/src/Core/Utils.php +++ b/seed/php-model/exhaustive/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/exhaustive/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/exhaustive/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/exhaustive/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/exhaustive/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/exhaustive/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/exhaustive/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/exhaustive/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/exhaustive/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/exhaustive/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/exhaustive/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/exhaustive/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/exhaustive/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/exhaustive/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/exhaustive/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/exhaustive/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/exhaustive/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/exhaustive/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/exhaustive/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/exhaustive/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/exhaustive/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/exhaustive/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/exhaustive/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/exhaustive/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/exhaustive/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/exhaustive/tests/Seed/Core/TestTypeTest.php b/seed/php-model/exhaustive/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/exhaustive/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/exhaustive/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/exhaustive/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/exhaustive/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/exhaustive/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/exhaustive/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/extends/src/Core/JsonDeserializer.php b/seed/php-model/extends/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/extends/src/Core/JsonDeserializer.php +++ b/seed/php-model/extends/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/extends/src/Core/JsonSerializer.php b/seed/php-model/extends/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/extends/src/Core/JsonSerializer.php +++ b/seed/php-model/extends/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/extends/src/Core/SerializableType.php b/seed/php-model/extends/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/extends/src/Core/SerializableType.php +++ b/seed/php-model/extends/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/extends/src/Core/Union.php b/seed/php-model/extends/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/extends/src/Core/Union.php +++ b/seed/php-model/extends/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/extends/src/Core/Utils.php b/seed/php-model/extends/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/extends/src/Core/Utils.php +++ b/seed/php-model/extends/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/extends/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/extends/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/extends/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/extends/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/extends/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/extends/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/extends/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/extends/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/extends/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/extends/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/extends/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/extends/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/extends/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/extends/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/extends/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/extends/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/extends/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/extends/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/extends/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/extends/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/extends/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/extends/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/extends/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/extends/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/extends/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/extends/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/extends/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/extends/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/extends/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/extends/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/extends/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/extends/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/extends/tests/Seed/Core/TestTypeTest.php b/seed/php-model/extends/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/extends/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/extends/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/extends/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/extends/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/extends/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/extends/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/extra-properties/src/Core/JsonDeserializer.php b/seed/php-model/extra-properties/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/extra-properties/src/Core/JsonDeserializer.php +++ b/seed/php-model/extra-properties/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/extra-properties/src/Core/JsonSerializer.php b/seed/php-model/extra-properties/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/extra-properties/src/Core/JsonSerializer.php +++ b/seed/php-model/extra-properties/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/extra-properties/src/Core/SerializableType.php b/seed/php-model/extra-properties/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/extra-properties/src/Core/SerializableType.php +++ b/seed/php-model/extra-properties/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/extra-properties/src/Core/Union.php b/seed/php-model/extra-properties/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/extra-properties/src/Core/Union.php +++ b/seed/php-model/extra-properties/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/extra-properties/src/Core/Utils.php b/seed/php-model/extra-properties/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/extra-properties/src/Core/Utils.php +++ b/seed/php-model/extra-properties/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/extra-properties/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/extra-properties/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/extra-properties/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/extra-properties/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/extra-properties/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/extra-properties/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/extra-properties/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/extra-properties/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/extra-properties/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/extra-properties/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/extra-properties/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/extra-properties/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/extra-properties/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/extra-properties/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/extra-properties/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/extra-properties/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/extra-properties/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/extra-properties/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/extra-properties/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/extra-properties/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/extra-properties/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/extra-properties/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/extra-properties/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/extra-properties/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/extra-properties/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/extra-properties/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/extra-properties/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/extra-properties/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/extra-properties/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/extra-properties/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/extra-properties/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/extra-properties/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/extra-properties/tests/Seed/Core/TestTypeTest.php b/seed/php-model/extra-properties/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/extra-properties/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/extra-properties/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/extra-properties/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/extra-properties/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/extra-properties/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/extra-properties/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/file-download/src/Core/JsonDeserializer.php b/seed/php-model/file-download/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/file-download/src/Core/JsonDeserializer.php +++ b/seed/php-model/file-download/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/file-download/src/Core/JsonSerializer.php b/seed/php-model/file-download/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/file-download/src/Core/JsonSerializer.php +++ b/seed/php-model/file-download/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/file-download/src/Core/SerializableType.php b/seed/php-model/file-download/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/file-download/src/Core/SerializableType.php +++ b/seed/php-model/file-download/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/file-download/src/Core/Union.php b/seed/php-model/file-download/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/file-download/src/Core/Union.php +++ b/seed/php-model/file-download/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/file-download/src/Core/Utils.php b/seed/php-model/file-download/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/file-download/src/Core/Utils.php +++ b/seed/php-model/file-download/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/file-download/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/file-download/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/file-download/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/file-download/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/file-download/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/file-download/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/file-download/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/file-download/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/file-download/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/file-download/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/file-download/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/file-download/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/file-download/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/file-download/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/file-download/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/file-download/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/file-download/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/file-download/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/file-download/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/file-download/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/file-download/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/file-download/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/file-download/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/file-download/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/file-download/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/file-download/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/file-download/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/file-download/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/file-download/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/file-download/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/file-download/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/file-download/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/file-download/tests/Seed/Core/TestTypeTest.php b/seed/php-model/file-download/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/file-download/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/file-download/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/file-download/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/file-download/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/file-download/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/file-download/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/file-upload/src/Core/JsonDeserializer.php b/seed/php-model/file-upload/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/file-upload/src/Core/JsonDeserializer.php +++ b/seed/php-model/file-upload/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/file-upload/src/Core/JsonSerializer.php b/seed/php-model/file-upload/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/file-upload/src/Core/JsonSerializer.php +++ b/seed/php-model/file-upload/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/file-upload/src/Core/SerializableType.php b/seed/php-model/file-upload/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/file-upload/src/Core/SerializableType.php +++ b/seed/php-model/file-upload/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/file-upload/src/Core/Union.php b/seed/php-model/file-upload/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/file-upload/src/Core/Union.php +++ b/seed/php-model/file-upload/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/file-upload/src/Core/Utils.php b/seed/php-model/file-upload/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/file-upload/src/Core/Utils.php +++ b/seed/php-model/file-upload/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/file-upload/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/file-upload/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/file-upload/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/file-upload/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/file-upload/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/file-upload/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/file-upload/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/file-upload/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/file-upload/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/file-upload/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/file-upload/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/file-upload/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/file-upload/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/file-upload/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/file-upload/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/file-upload/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/file-upload/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/file-upload/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/file-upload/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/file-upload/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/file-upload/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/file-upload/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/file-upload/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/file-upload/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/file-upload/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/file-upload/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/file-upload/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/file-upload/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/file-upload/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/file-upload/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/file-upload/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/file-upload/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/file-upload/tests/Seed/Core/TestTypeTest.php b/seed/php-model/file-upload/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/file-upload/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/file-upload/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/file-upload/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/file-upload/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/file-upload/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/file-upload/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/folders/src/Core/JsonDeserializer.php b/seed/php-model/folders/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/folders/src/Core/JsonDeserializer.php +++ b/seed/php-model/folders/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/folders/src/Core/JsonSerializer.php b/seed/php-model/folders/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/folders/src/Core/JsonSerializer.php +++ b/seed/php-model/folders/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/folders/src/Core/SerializableType.php b/seed/php-model/folders/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/folders/src/Core/SerializableType.php +++ b/seed/php-model/folders/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/folders/src/Core/Union.php b/seed/php-model/folders/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/folders/src/Core/Union.php +++ b/seed/php-model/folders/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/folders/src/Core/Utils.php b/seed/php-model/folders/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/folders/src/Core/Utils.php +++ b/seed/php-model/folders/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/folders/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/folders/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/folders/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/folders/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/folders/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/folders/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/folders/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/folders/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/folders/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/folders/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/folders/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/folders/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/folders/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/folders/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/folders/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/folders/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/folders/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/folders/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/folders/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/folders/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/folders/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/folders/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/folders/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/folders/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/folders/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/folders/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/folders/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/folders/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/folders/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/folders/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/folders/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/folders/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/folders/tests/Seed/Core/TestTypeTest.php b/seed/php-model/folders/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/folders/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/folders/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/folders/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/folders/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/folders/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/folders/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDeserializer.php b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonSerializer.php b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/grpc-proto-exhaustive/src/Core/SerializableType.php b/seed/php-model/grpc-proto-exhaustive/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/SerializableType.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/grpc-proto-exhaustive/src/Core/Union.php b/seed/php-model/grpc-proto-exhaustive/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/Union.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/grpc-proto-exhaustive/src/Core/Utils.php b/seed/php-model/grpc-proto-exhaustive/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/Utils.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/TestTypeTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php b/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php +++ b/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/grpc-proto/src/Core/JsonSerializer.php b/seed/php-model/grpc-proto/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/grpc-proto/src/Core/JsonSerializer.php +++ b/seed/php-model/grpc-proto/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/grpc-proto/src/Core/SerializableType.php b/seed/php-model/grpc-proto/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/grpc-proto/src/Core/SerializableType.php +++ b/seed/php-model/grpc-proto/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/grpc-proto/src/Core/Union.php b/seed/php-model/grpc-proto/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/grpc-proto/src/Core/Union.php +++ b/seed/php-model/grpc-proto/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/grpc-proto/src/Core/Utils.php b/seed/php-model/grpc-proto/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/grpc-proto/src/Core/Utils.php +++ b/seed/php-model/grpc-proto/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/grpc-proto/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/grpc-proto/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/grpc-proto/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/grpc-proto/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/grpc-proto/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/grpc-proto/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/grpc-proto/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/grpc-proto/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/grpc-proto/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/grpc-proto/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/grpc-proto/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/grpc-proto/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/grpc-proto/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/grpc-proto/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/grpc-proto/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/grpc-proto/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/TestTypeTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/grpc-proto/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/grpc-proto/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/grpc-proto/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/grpc-proto/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php b/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php +++ b/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php b/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php +++ b/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/idempotency-headers/src/Core/SerializableType.php b/seed/php-model/idempotency-headers/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/idempotency-headers/src/Core/SerializableType.php +++ b/seed/php-model/idempotency-headers/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/idempotency-headers/src/Core/Union.php b/seed/php-model/idempotency-headers/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/idempotency-headers/src/Core/Union.php +++ b/seed/php-model/idempotency-headers/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/idempotency-headers/src/Core/Utils.php b/seed/php-model/idempotency-headers/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/idempotency-headers/src/Core/Utils.php +++ b/seed/php-model/idempotency-headers/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/idempotency-headers/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/idempotency-headers/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/idempotency-headers/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/idempotency-headers/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/idempotency-headers/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/idempotency-headers/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/idempotency-headers/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/idempotency-headers/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/TestTypeTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/idempotency-headers/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/idempotency-headers/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/imdb/src/Core/JsonDeserializer.php b/seed/php-model/imdb/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/imdb/src/Core/JsonDeserializer.php +++ b/seed/php-model/imdb/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/imdb/src/Core/JsonSerializer.php b/seed/php-model/imdb/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/imdb/src/Core/JsonSerializer.php +++ b/seed/php-model/imdb/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/imdb/src/Core/SerializableType.php b/seed/php-model/imdb/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/imdb/src/Core/SerializableType.php +++ b/seed/php-model/imdb/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/imdb/src/Core/Union.php b/seed/php-model/imdb/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/imdb/src/Core/Union.php +++ b/seed/php-model/imdb/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/imdb/src/Core/Utils.php b/seed/php-model/imdb/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/imdb/src/Core/Utils.php +++ b/seed/php-model/imdb/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/imdb/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/imdb/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/imdb/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/imdb/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/imdb/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/imdb/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/imdb/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/imdb/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/imdb/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/imdb/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/imdb/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/imdb/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/imdb/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/imdb/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/imdb/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/imdb/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/imdb/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/imdb/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/imdb/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/imdb/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/imdb/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/imdb/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/imdb/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/imdb/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/imdb/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/imdb/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/imdb/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/imdb/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/imdb/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/imdb/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/imdb/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/imdb/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/imdb/tests/Seed/Core/TestTypeTest.php b/seed/php-model/imdb/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/imdb/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/imdb/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/imdb/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/imdb/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/imdb/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/imdb/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/literal/src/Core/JsonDeserializer.php b/seed/php-model/literal/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/literal/src/Core/JsonDeserializer.php +++ b/seed/php-model/literal/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/literal/src/Core/JsonSerializer.php b/seed/php-model/literal/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/literal/src/Core/JsonSerializer.php +++ b/seed/php-model/literal/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/literal/src/Core/SerializableType.php b/seed/php-model/literal/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/literal/src/Core/SerializableType.php +++ b/seed/php-model/literal/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/literal/src/Core/Union.php b/seed/php-model/literal/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/literal/src/Core/Union.php +++ b/seed/php-model/literal/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/literal/src/Core/Utils.php b/seed/php-model/literal/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/literal/src/Core/Utils.php +++ b/seed/php-model/literal/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/literal/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/literal/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/literal/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/literal/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/literal/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/literal/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/literal/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/literal/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/literal/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/literal/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/literal/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/literal/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/literal/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/literal/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/literal/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/literal/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/literal/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/literal/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/literal/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/literal/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/literal/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/literal/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/literal/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/literal/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/literal/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/literal/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/literal/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/literal/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/literal/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/literal/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/literal/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/literal/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/literal/tests/Seed/Core/TestTypeTest.php b/seed/php-model/literal/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/literal/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/literal/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/literal/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/literal/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/literal/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/literal/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/mixed-case/src/Core/JsonDeserializer.php b/seed/php-model/mixed-case/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/mixed-case/src/Core/JsonDeserializer.php +++ b/seed/php-model/mixed-case/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/mixed-case/src/Core/JsonSerializer.php b/seed/php-model/mixed-case/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/mixed-case/src/Core/JsonSerializer.php +++ b/seed/php-model/mixed-case/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/mixed-case/src/Core/SerializableType.php b/seed/php-model/mixed-case/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/mixed-case/src/Core/SerializableType.php +++ b/seed/php-model/mixed-case/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/mixed-case/src/Core/Union.php b/seed/php-model/mixed-case/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/mixed-case/src/Core/Union.php +++ b/seed/php-model/mixed-case/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/mixed-case/src/Core/Utils.php b/seed/php-model/mixed-case/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/mixed-case/src/Core/Utils.php +++ b/seed/php-model/mixed-case/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/mixed-case/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/mixed-case/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/mixed-case/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/mixed-case/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/mixed-case/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/mixed-case/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/mixed-case/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/mixed-case/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/mixed-case/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/mixed-case/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/mixed-case/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/mixed-case/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/mixed-case/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/mixed-case/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/mixed-case/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/mixed-case/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/mixed-case/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/mixed-case/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/mixed-case/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/mixed-case/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/mixed-case/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/mixed-case/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/mixed-case/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/mixed-case/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/mixed-case/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/mixed-case/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/mixed-case/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/mixed-case/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/mixed-case/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/mixed-case/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/mixed-case/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/mixed-case/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/mixed-case/tests/Seed/Core/TestTypeTest.php b/seed/php-model/mixed-case/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/mixed-case/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/mixed-case/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/mixed-case/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/mixed-case/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/mixed-case/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/mixed-case/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/mixed-file-directory/src/Core/JsonDeserializer.php b/seed/php-model/mixed-file-directory/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/mixed-file-directory/src/Core/JsonDeserializer.php +++ b/seed/php-model/mixed-file-directory/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/mixed-file-directory/src/Core/JsonSerializer.php b/seed/php-model/mixed-file-directory/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/mixed-file-directory/src/Core/JsonSerializer.php +++ b/seed/php-model/mixed-file-directory/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/mixed-file-directory/src/Core/SerializableType.php b/seed/php-model/mixed-file-directory/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/mixed-file-directory/src/Core/SerializableType.php +++ b/seed/php-model/mixed-file-directory/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/mixed-file-directory/src/Core/Union.php b/seed/php-model/mixed-file-directory/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/mixed-file-directory/src/Core/Union.php +++ b/seed/php-model/mixed-file-directory/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/mixed-file-directory/src/Core/Utils.php b/seed/php-model/mixed-file-directory/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/mixed-file-directory/src/Core/Utils.php +++ b/seed/php-model/mixed-file-directory/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/mixed-file-directory/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/mixed-file-directory/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/mixed-file-directory/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/mixed-file-directory/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/mixed-file-directory/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/mixed-file-directory/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/mixed-file-directory/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/mixed-file-directory/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/TestTypeTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/mixed-file-directory/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/mixed-file-directory/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/multi-line-docs/src/Core/JsonDeserializer.php b/seed/php-model/multi-line-docs/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/multi-line-docs/src/Core/JsonDeserializer.php +++ b/seed/php-model/multi-line-docs/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/multi-line-docs/src/Core/JsonSerializer.php b/seed/php-model/multi-line-docs/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/multi-line-docs/src/Core/JsonSerializer.php +++ b/seed/php-model/multi-line-docs/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/multi-line-docs/src/Core/SerializableType.php b/seed/php-model/multi-line-docs/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/multi-line-docs/src/Core/SerializableType.php +++ b/seed/php-model/multi-line-docs/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/multi-line-docs/src/Core/Union.php b/seed/php-model/multi-line-docs/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/multi-line-docs/src/Core/Union.php +++ b/seed/php-model/multi-line-docs/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/multi-line-docs/src/Core/Utils.php b/seed/php-model/multi-line-docs/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/multi-line-docs/src/Core/Utils.php +++ b/seed/php-model/multi-line-docs/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/multi-line-docs/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/multi-line-docs/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/multi-line-docs/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/multi-line-docs/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/multi-line-docs/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/multi-line-docs/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/multi-line-docs/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/multi-line-docs/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/TestTypeTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/multi-line-docs/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/multi-line-docs/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/multi-url-environment-no-default/src/Core/JsonDeserializer.php b/seed/php-model/multi-url-environment-no-default/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/multi-url-environment-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-model/multi-url-environment-no-default/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/multi-url-environment-no-default/src/Core/JsonSerializer.php b/seed/php-model/multi-url-environment-no-default/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/multi-url-environment-no-default/src/Core/JsonSerializer.php +++ b/seed/php-model/multi-url-environment-no-default/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/multi-url-environment-no-default/src/Core/SerializableType.php b/seed/php-model/multi-url-environment-no-default/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/multi-url-environment-no-default/src/Core/SerializableType.php +++ b/seed/php-model/multi-url-environment-no-default/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/multi-url-environment-no-default/src/Core/Union.php b/seed/php-model/multi-url-environment-no-default/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/multi-url-environment-no-default/src/Core/Union.php +++ b/seed/php-model/multi-url-environment-no-default/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/multi-url-environment-no-default/src/Core/Utils.php b/seed/php-model/multi-url-environment-no-default/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/multi-url-environment-no-default/src/Core/Utils.php +++ b/seed/php-model/multi-url-environment-no-default/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/TestTypeTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/multi-url-environment/src/Core/JsonDeserializer.php b/seed/php-model/multi-url-environment/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/multi-url-environment/src/Core/JsonDeserializer.php +++ b/seed/php-model/multi-url-environment/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/multi-url-environment/src/Core/JsonSerializer.php b/seed/php-model/multi-url-environment/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/multi-url-environment/src/Core/JsonSerializer.php +++ b/seed/php-model/multi-url-environment/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/multi-url-environment/src/Core/SerializableType.php b/seed/php-model/multi-url-environment/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/multi-url-environment/src/Core/SerializableType.php +++ b/seed/php-model/multi-url-environment/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/multi-url-environment/src/Core/Union.php b/seed/php-model/multi-url-environment/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/multi-url-environment/src/Core/Union.php +++ b/seed/php-model/multi-url-environment/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/multi-url-environment/src/Core/Utils.php b/seed/php-model/multi-url-environment/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/multi-url-environment/src/Core/Utils.php +++ b/seed/php-model/multi-url-environment/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/multi-url-environment/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/multi-url-environment/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/multi-url-environment/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/multi-url-environment/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/multi-url-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/multi-url-environment/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/multi-url-environment/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/multi-url-environment/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/TestTypeTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/multi-url-environment/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/multi-url-environment/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/no-environment/src/Core/JsonDeserializer.php b/seed/php-model/no-environment/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/no-environment/src/Core/JsonDeserializer.php +++ b/seed/php-model/no-environment/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/no-environment/src/Core/JsonSerializer.php b/seed/php-model/no-environment/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/no-environment/src/Core/JsonSerializer.php +++ b/seed/php-model/no-environment/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/no-environment/src/Core/SerializableType.php b/seed/php-model/no-environment/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/no-environment/src/Core/SerializableType.php +++ b/seed/php-model/no-environment/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/no-environment/src/Core/Union.php b/seed/php-model/no-environment/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/no-environment/src/Core/Union.php +++ b/seed/php-model/no-environment/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/no-environment/src/Core/Utils.php b/seed/php-model/no-environment/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/no-environment/src/Core/Utils.php +++ b/seed/php-model/no-environment/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/no-environment/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/no-environment/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/no-environment/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/no-environment/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/no-environment/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/no-environment/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/no-environment/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/no-environment/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/no-environment/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/no-environment/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/no-environment/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/no-environment/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/no-environment/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/no-environment/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/no-environment/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/no-environment/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/no-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/no-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/no-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/no-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/no-environment/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/no-environment/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/no-environment/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/no-environment/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/no-environment/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/no-environment/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/no-environment/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/no-environment/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/no-environment/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/no-environment/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/no-environment/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/no-environment/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/no-environment/tests/Seed/Core/TestTypeTest.php b/seed/php-model/no-environment/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/no-environment/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/no-environment/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/no-environment/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/no-environment/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/no-environment/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/no-environment/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/oauth-client-credentials-default/src/Core/JsonDeserializer.php b/seed/php-model/oauth-client-credentials-default/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/oauth-client-credentials-default/src/Core/JsonDeserializer.php +++ b/seed/php-model/oauth-client-credentials-default/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/oauth-client-credentials-default/src/Core/JsonSerializer.php b/seed/php-model/oauth-client-credentials-default/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/oauth-client-credentials-default/src/Core/JsonSerializer.php +++ b/seed/php-model/oauth-client-credentials-default/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/oauth-client-credentials-default/src/Core/SerializableType.php b/seed/php-model/oauth-client-credentials-default/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/oauth-client-credentials-default/src/Core/SerializableType.php +++ b/seed/php-model/oauth-client-credentials-default/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/oauth-client-credentials-default/src/Core/Union.php b/seed/php-model/oauth-client-credentials-default/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/oauth-client-credentials-default/src/Core/Union.php +++ b/seed/php-model/oauth-client-credentials-default/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/oauth-client-credentials-default/src/Core/Utils.php b/seed/php-model/oauth-client-credentials-default/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/oauth-client-credentials-default/src/Core/Utils.php +++ b/seed/php-model/oauth-client-credentials-default/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/TestTypeTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/SerializableType.php b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/SerializableType.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Union.php b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Union.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Utils.php b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Utils.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/TestTypeTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php +++ b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php +++ b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/oauth-client-credentials-nested-root/src/Core/SerializableType.php b/seed/php-model/oauth-client-credentials-nested-root/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/src/Core/SerializableType.php +++ b/seed/php-model/oauth-client-credentials-nested-root/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/oauth-client-credentials-nested-root/src/Core/Union.php b/seed/php-model/oauth-client-credentials-nested-root/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/src/Core/Union.php +++ b/seed/php-model/oauth-client-credentials-nested-root/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/src/Core/Utils.php b/seed/php-model/oauth-client-credentials-nested-root/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/src/Core/Utils.php +++ b/seed/php-model/oauth-client-credentials-nested-root/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/TestTypeTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/oauth-client-credentials/src/Core/JsonDeserializer.php b/seed/php-model/oauth-client-credentials/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/JsonDeserializer.php +++ b/seed/php-model/oauth-client-credentials/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/oauth-client-credentials/src/Core/JsonSerializer.php b/seed/php-model/oauth-client-credentials/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/JsonSerializer.php +++ b/seed/php-model/oauth-client-credentials/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/oauth-client-credentials/src/Core/SerializableType.php b/seed/php-model/oauth-client-credentials/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/SerializableType.php +++ b/seed/php-model/oauth-client-credentials/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/oauth-client-credentials/src/Core/Union.php b/seed/php-model/oauth-client-credentials/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/Union.php +++ b/seed/php-model/oauth-client-credentials/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/oauth-client-credentials/src/Core/Utils.php b/seed/php-model/oauth-client-credentials/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/Utils.php +++ b/seed/php-model/oauth-client-credentials/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/oauth-client-credentials/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/oauth-client-credentials/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/oauth-client-credentials/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/oauth-client-credentials/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/oauth-client-credentials/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/oauth-client-credentials/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/oauth-client-credentials/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/oauth-client-credentials/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/TestTypeTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/oauth-client-credentials/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/oauth-client-credentials/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/object/src/Core/JsonDeserializer.php b/seed/php-model/object/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/object/src/Core/JsonDeserializer.php +++ b/seed/php-model/object/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/object/src/Core/JsonSerializer.php b/seed/php-model/object/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/object/src/Core/JsonSerializer.php +++ b/seed/php-model/object/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/object/src/Core/SerializableType.php b/seed/php-model/object/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/object/src/Core/SerializableType.php +++ b/seed/php-model/object/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/object/src/Core/Union.php b/seed/php-model/object/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/object/src/Core/Union.php +++ b/seed/php-model/object/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/object/src/Core/Utils.php b/seed/php-model/object/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/object/src/Core/Utils.php +++ b/seed/php-model/object/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/object/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/object/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/object/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/object/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/object/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/object/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/object/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/object/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/object/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/object/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/object/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/object/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/object/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/object/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/object/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/object/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/object/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/object/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/object/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/object/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/object/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/object/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/object/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/object/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/object/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/object/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/object/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/object/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/object/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/object/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/object/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/object/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/object/tests/Seed/Core/TestTypeTest.php b/seed/php-model/object/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/object/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/object/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/object/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/object/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/object/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/object/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/objects-with-imports/src/Core/JsonDeserializer.php b/seed/php-model/objects-with-imports/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/objects-with-imports/src/Core/JsonDeserializer.php +++ b/seed/php-model/objects-with-imports/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/objects-with-imports/src/Core/JsonSerializer.php b/seed/php-model/objects-with-imports/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/objects-with-imports/src/Core/JsonSerializer.php +++ b/seed/php-model/objects-with-imports/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/objects-with-imports/src/Core/SerializableType.php b/seed/php-model/objects-with-imports/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/objects-with-imports/src/Core/SerializableType.php +++ b/seed/php-model/objects-with-imports/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/objects-with-imports/src/Core/Union.php b/seed/php-model/objects-with-imports/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/objects-with-imports/src/Core/Union.php +++ b/seed/php-model/objects-with-imports/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/objects-with-imports/src/Core/Utils.php b/seed/php-model/objects-with-imports/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/objects-with-imports/src/Core/Utils.php +++ b/seed/php-model/objects-with-imports/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/objects-with-imports/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/objects-with-imports/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/objects-with-imports/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/objects-with-imports/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/objects-with-imports/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/objects-with-imports/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/objects-with-imports/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/objects-with-imports/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/TestTypeTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/objects-with-imports/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/objects-with-imports/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/optional/src/Core/JsonDeserializer.php b/seed/php-model/optional/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/optional/src/Core/JsonDeserializer.php +++ b/seed/php-model/optional/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/optional/src/Core/JsonSerializer.php b/seed/php-model/optional/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/optional/src/Core/JsonSerializer.php +++ b/seed/php-model/optional/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/optional/src/Core/SerializableType.php b/seed/php-model/optional/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/optional/src/Core/SerializableType.php +++ b/seed/php-model/optional/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/optional/src/Core/Union.php b/seed/php-model/optional/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/optional/src/Core/Union.php +++ b/seed/php-model/optional/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/optional/src/Core/Utils.php b/seed/php-model/optional/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/optional/src/Core/Utils.php +++ b/seed/php-model/optional/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/optional/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/optional/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/optional/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/optional/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/optional/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/optional/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/optional/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/optional/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/optional/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/optional/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/optional/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/optional/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/optional/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/optional/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/optional/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/optional/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/optional/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/optional/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/optional/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/optional/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/optional/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/optional/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/optional/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/optional/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/optional/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/optional/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/optional/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/optional/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/optional/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/optional/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/optional/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/optional/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/optional/tests/Seed/Core/TestTypeTest.php b/seed/php-model/optional/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/optional/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/optional/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/optional/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/optional/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/optional/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/optional/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/package-yml/src/Core/JsonDeserializer.php b/seed/php-model/package-yml/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/package-yml/src/Core/JsonDeserializer.php +++ b/seed/php-model/package-yml/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/package-yml/src/Core/JsonSerializer.php b/seed/php-model/package-yml/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/package-yml/src/Core/JsonSerializer.php +++ b/seed/php-model/package-yml/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/package-yml/src/Core/SerializableType.php b/seed/php-model/package-yml/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/package-yml/src/Core/SerializableType.php +++ b/seed/php-model/package-yml/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/package-yml/src/Core/Union.php b/seed/php-model/package-yml/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/package-yml/src/Core/Union.php +++ b/seed/php-model/package-yml/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/package-yml/src/Core/Utils.php b/seed/php-model/package-yml/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/package-yml/src/Core/Utils.php +++ b/seed/php-model/package-yml/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/package-yml/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/package-yml/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/package-yml/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/package-yml/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/package-yml/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/package-yml/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/package-yml/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/package-yml/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/package-yml/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/package-yml/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/package-yml/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/package-yml/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/package-yml/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/package-yml/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/package-yml/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/package-yml/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/package-yml/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/package-yml/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/package-yml/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/package-yml/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/package-yml/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/package-yml/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/package-yml/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/package-yml/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/package-yml/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/package-yml/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/package-yml/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/package-yml/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/package-yml/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/package-yml/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/package-yml/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/package-yml/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/package-yml/tests/Seed/Core/TestTypeTest.php b/seed/php-model/package-yml/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/package-yml/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/package-yml/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/package-yml/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/package-yml/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/package-yml/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/package-yml/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/pagination/src/Core/JsonDeserializer.php b/seed/php-model/pagination/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/pagination/src/Core/JsonDeserializer.php +++ b/seed/php-model/pagination/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/pagination/src/Core/JsonSerializer.php b/seed/php-model/pagination/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/pagination/src/Core/JsonSerializer.php +++ b/seed/php-model/pagination/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/pagination/src/Core/SerializableType.php b/seed/php-model/pagination/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/pagination/src/Core/SerializableType.php +++ b/seed/php-model/pagination/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/pagination/src/Core/Union.php b/seed/php-model/pagination/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/pagination/src/Core/Union.php +++ b/seed/php-model/pagination/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/pagination/src/Core/Utils.php b/seed/php-model/pagination/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/pagination/src/Core/Utils.php +++ b/seed/php-model/pagination/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/pagination/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/pagination/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/pagination/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/pagination/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/pagination/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/pagination/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/pagination/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/pagination/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/pagination/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/pagination/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/pagination/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/pagination/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/pagination/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/pagination/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/pagination/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/pagination/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/pagination/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/pagination/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/pagination/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/pagination/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/pagination/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/pagination/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/pagination/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/pagination/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/pagination/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/pagination/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/pagination/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/pagination/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/pagination/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/pagination/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/pagination/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/pagination/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/pagination/tests/Seed/Core/TestTypeTest.php b/seed/php-model/pagination/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/pagination/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/pagination/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/pagination/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/pagination/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/pagination/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/pagination/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/plain-text/src/Core/JsonDeserializer.php b/seed/php-model/plain-text/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/plain-text/src/Core/JsonDeserializer.php +++ b/seed/php-model/plain-text/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/plain-text/src/Core/JsonSerializer.php b/seed/php-model/plain-text/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/plain-text/src/Core/JsonSerializer.php +++ b/seed/php-model/plain-text/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/plain-text/src/Core/SerializableType.php b/seed/php-model/plain-text/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/plain-text/src/Core/SerializableType.php +++ b/seed/php-model/plain-text/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/plain-text/src/Core/Union.php b/seed/php-model/plain-text/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/plain-text/src/Core/Union.php +++ b/seed/php-model/plain-text/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/plain-text/src/Core/Utils.php b/seed/php-model/plain-text/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/plain-text/src/Core/Utils.php +++ b/seed/php-model/plain-text/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/plain-text/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/plain-text/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/plain-text/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/plain-text/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/plain-text/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/plain-text/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/plain-text/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/plain-text/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/plain-text/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/plain-text/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/plain-text/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/plain-text/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/plain-text/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/plain-text/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/plain-text/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/plain-text/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/plain-text/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/plain-text/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/plain-text/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/plain-text/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/plain-text/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/plain-text/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/plain-text/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/plain-text/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/plain-text/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/plain-text/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/plain-text/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/plain-text/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/plain-text/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/plain-text/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/plain-text/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/plain-text/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/plain-text/tests/Seed/Core/TestTypeTest.php b/seed/php-model/plain-text/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/plain-text/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/plain-text/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/plain-text/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/plain-text/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/plain-text/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/plain-text/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/query-parameters/src/Core/JsonDeserializer.php b/seed/php-model/query-parameters/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/query-parameters/src/Core/JsonDeserializer.php +++ b/seed/php-model/query-parameters/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/query-parameters/src/Core/JsonSerializer.php b/seed/php-model/query-parameters/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/query-parameters/src/Core/JsonSerializer.php +++ b/seed/php-model/query-parameters/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/query-parameters/src/Core/SerializableType.php b/seed/php-model/query-parameters/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/query-parameters/src/Core/SerializableType.php +++ b/seed/php-model/query-parameters/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/query-parameters/src/Core/Union.php b/seed/php-model/query-parameters/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/query-parameters/src/Core/Union.php +++ b/seed/php-model/query-parameters/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/query-parameters/src/Core/Utils.php b/seed/php-model/query-parameters/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/query-parameters/src/Core/Utils.php +++ b/seed/php-model/query-parameters/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/query-parameters/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/query-parameters/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/query-parameters/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/query-parameters/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/query-parameters/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/query-parameters/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/query-parameters/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/query-parameters/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/query-parameters/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/query-parameters/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/query-parameters/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/query-parameters/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/query-parameters/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/query-parameters/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/query-parameters/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/query-parameters/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/query-parameters/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/query-parameters/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/query-parameters/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/query-parameters/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/query-parameters/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/query-parameters/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/query-parameters/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/query-parameters/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/query-parameters/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/query-parameters/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/query-parameters/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/query-parameters/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/query-parameters/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/query-parameters/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/query-parameters/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/query-parameters/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/query-parameters/tests/Seed/Core/TestTypeTest.php b/seed/php-model/query-parameters/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/query-parameters/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/query-parameters/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/query-parameters/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/query-parameters/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/query-parameters/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/query-parameters/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php b/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php +++ b/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php b/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php +++ b/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/reserved-keywords/src/Core/SerializableType.php b/seed/php-model/reserved-keywords/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/reserved-keywords/src/Core/SerializableType.php +++ b/seed/php-model/reserved-keywords/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/reserved-keywords/src/Core/Union.php b/seed/php-model/reserved-keywords/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/reserved-keywords/src/Core/Union.php +++ b/seed/php-model/reserved-keywords/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/reserved-keywords/src/Core/Utils.php b/seed/php-model/reserved-keywords/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/reserved-keywords/src/Core/Utils.php +++ b/seed/php-model/reserved-keywords/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/reserved-keywords/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/reserved-keywords/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/reserved-keywords/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/reserved-keywords/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/reserved-keywords/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/reserved-keywords/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/reserved-keywords/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/reserved-keywords/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/TestTypeTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/reserved-keywords/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/reserved-keywords/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/response-property/src/Core/JsonDeserializer.php b/seed/php-model/response-property/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/response-property/src/Core/JsonDeserializer.php +++ b/seed/php-model/response-property/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/response-property/src/Core/JsonSerializer.php b/seed/php-model/response-property/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/response-property/src/Core/JsonSerializer.php +++ b/seed/php-model/response-property/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/response-property/src/Core/SerializableType.php b/seed/php-model/response-property/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/response-property/src/Core/SerializableType.php +++ b/seed/php-model/response-property/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/response-property/src/Core/Union.php b/seed/php-model/response-property/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/response-property/src/Core/Union.php +++ b/seed/php-model/response-property/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/response-property/src/Core/Utils.php b/seed/php-model/response-property/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/response-property/src/Core/Utils.php +++ b/seed/php-model/response-property/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/response-property/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/response-property/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/response-property/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/response-property/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/response-property/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/response-property/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/response-property/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/response-property/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/response-property/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/response-property/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/response-property/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/response-property/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/response-property/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/response-property/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/response-property/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/response-property/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/response-property/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/response-property/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/response-property/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/response-property/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/response-property/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/response-property/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/response-property/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/response-property/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/response-property/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/response-property/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/response-property/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/response-property/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/response-property/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/response-property/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/response-property/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/response-property/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/response-property/tests/Seed/Core/TestTypeTest.php b/seed/php-model/response-property/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/response-property/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/response-property/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/response-property/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/response-property/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/response-property/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/response-property/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php b/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php +++ b/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/simple-fhir/src/Core/JsonSerializer.php b/seed/php-model/simple-fhir/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/simple-fhir/src/Core/JsonSerializer.php +++ b/seed/php-model/simple-fhir/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/simple-fhir/src/Core/SerializableType.php b/seed/php-model/simple-fhir/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/simple-fhir/src/Core/SerializableType.php +++ b/seed/php-model/simple-fhir/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/simple-fhir/src/Core/Union.php b/seed/php-model/simple-fhir/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/simple-fhir/src/Core/Union.php +++ b/seed/php-model/simple-fhir/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/simple-fhir/src/Core/Utils.php b/seed/php-model/simple-fhir/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/simple-fhir/src/Core/Utils.php +++ b/seed/php-model/simple-fhir/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/simple-fhir/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/simple-fhir/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/simple-fhir/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/simple-fhir/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/simple-fhir/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/simple-fhir/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/simple-fhir/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/simple-fhir/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/simple-fhir/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/simple-fhir/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/simple-fhir/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/simple-fhir/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/simple-fhir/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/simple-fhir/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/simple-fhir/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/simple-fhir/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/TestTypeTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/simple-fhir/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/simple-fhir/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/simple-fhir/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/simple-fhir/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/single-url-environment-default/src/Core/JsonDeserializer.php b/seed/php-model/single-url-environment-default/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/single-url-environment-default/src/Core/JsonDeserializer.php +++ b/seed/php-model/single-url-environment-default/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/single-url-environment-default/src/Core/JsonSerializer.php b/seed/php-model/single-url-environment-default/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/single-url-environment-default/src/Core/JsonSerializer.php +++ b/seed/php-model/single-url-environment-default/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/single-url-environment-default/src/Core/SerializableType.php b/seed/php-model/single-url-environment-default/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/single-url-environment-default/src/Core/SerializableType.php +++ b/seed/php-model/single-url-environment-default/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/single-url-environment-default/src/Core/Union.php b/seed/php-model/single-url-environment-default/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/single-url-environment-default/src/Core/Union.php +++ b/seed/php-model/single-url-environment-default/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/single-url-environment-default/src/Core/Utils.php b/seed/php-model/single-url-environment-default/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/single-url-environment-default/src/Core/Utils.php +++ b/seed/php-model/single-url-environment-default/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/single-url-environment-default/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/single-url-environment-default/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/single-url-environment-default/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/single-url-environment-default/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/single-url-environment-default/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/single-url-environment-default/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/single-url-environment-default/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/single-url-environment-default/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/TestTypeTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/single-url-environment-default/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/single-url-environment-default/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/single-url-environment-no-default/src/Core/JsonDeserializer.php b/seed/php-model/single-url-environment-no-default/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/single-url-environment-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-model/single-url-environment-no-default/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/single-url-environment-no-default/src/Core/JsonSerializer.php b/seed/php-model/single-url-environment-no-default/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/single-url-environment-no-default/src/Core/JsonSerializer.php +++ b/seed/php-model/single-url-environment-no-default/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/single-url-environment-no-default/src/Core/SerializableType.php b/seed/php-model/single-url-environment-no-default/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/single-url-environment-no-default/src/Core/SerializableType.php +++ b/seed/php-model/single-url-environment-no-default/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/single-url-environment-no-default/src/Core/Union.php b/seed/php-model/single-url-environment-no-default/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/single-url-environment-no-default/src/Core/Union.php +++ b/seed/php-model/single-url-environment-no-default/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/single-url-environment-no-default/src/Core/Utils.php b/seed/php-model/single-url-environment-no-default/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/single-url-environment-no-default/src/Core/Utils.php +++ b/seed/php-model/single-url-environment-no-default/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/TestTypeTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php b/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php +++ b/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php b/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php +++ b/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/streaming-parameter/src/Core/SerializableType.php b/seed/php-model/streaming-parameter/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/streaming-parameter/src/Core/SerializableType.php +++ b/seed/php-model/streaming-parameter/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/streaming-parameter/src/Core/Union.php b/seed/php-model/streaming-parameter/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/streaming-parameter/src/Core/Union.php +++ b/seed/php-model/streaming-parameter/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/streaming-parameter/src/Core/Utils.php b/seed/php-model/streaming-parameter/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/streaming-parameter/src/Core/Utils.php +++ b/seed/php-model/streaming-parameter/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/streaming-parameter/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/streaming-parameter/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/streaming-parameter/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/streaming-parameter/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/streaming-parameter/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/streaming-parameter/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/streaming-parameter/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/streaming-parameter/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/TestTypeTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/streaming-parameter/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/streaming-parameter/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/streaming/src/Core/JsonDeserializer.php b/seed/php-model/streaming/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/streaming/src/Core/JsonDeserializer.php +++ b/seed/php-model/streaming/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/streaming/src/Core/JsonSerializer.php b/seed/php-model/streaming/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/streaming/src/Core/JsonSerializer.php +++ b/seed/php-model/streaming/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/streaming/src/Core/SerializableType.php b/seed/php-model/streaming/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/streaming/src/Core/SerializableType.php +++ b/seed/php-model/streaming/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/streaming/src/Core/Union.php b/seed/php-model/streaming/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/streaming/src/Core/Union.php +++ b/seed/php-model/streaming/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/streaming/src/Core/Utils.php b/seed/php-model/streaming/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/streaming/src/Core/Utils.php +++ b/seed/php-model/streaming/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/streaming/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/streaming/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/streaming/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/streaming/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/streaming/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/streaming/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/streaming/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/streaming/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/streaming/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/streaming/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/streaming/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/streaming/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/streaming/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/streaming/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/streaming/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/streaming/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/streaming/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/streaming/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/streaming/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/streaming/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/streaming/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/streaming/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/streaming/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/streaming/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/streaming/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/streaming/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/streaming/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/streaming/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/streaming/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/streaming/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/streaming/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/streaming/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/streaming/tests/Seed/Core/TestTypeTest.php b/seed/php-model/streaming/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/streaming/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/streaming/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/streaming/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/streaming/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/streaming/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/streaming/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/trace/src/Core/JsonDeserializer.php b/seed/php-model/trace/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/trace/src/Core/JsonDeserializer.php +++ b/seed/php-model/trace/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/trace/src/Core/JsonSerializer.php b/seed/php-model/trace/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/trace/src/Core/JsonSerializer.php +++ b/seed/php-model/trace/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/trace/src/Core/SerializableType.php b/seed/php-model/trace/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/trace/src/Core/SerializableType.php +++ b/seed/php-model/trace/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/trace/src/Core/Union.php b/seed/php-model/trace/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/trace/src/Core/Union.php +++ b/seed/php-model/trace/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/trace/src/Core/Utils.php b/seed/php-model/trace/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/trace/src/Core/Utils.php +++ b/seed/php-model/trace/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/trace/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/trace/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/trace/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/trace/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/trace/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/trace/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/trace/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/trace/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/trace/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/trace/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/trace/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/trace/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/trace/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/trace/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/trace/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/trace/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/trace/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/trace/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/trace/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/trace/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/trace/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/trace/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/trace/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/trace/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/trace/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/trace/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/trace/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/trace/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/trace/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/trace/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/trace/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/trace/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/trace/tests/Seed/Core/TestTypeTest.php b/seed/php-model/trace/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/trace/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/trace/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/trace/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/trace/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/trace/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/trace/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php b/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php +++ b/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php b/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php +++ b/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php b/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php +++ b/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/undiscriminated-unions/src/Core/Union.php b/seed/php-model/undiscriminated-unions/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/Union.php +++ b/seed/php-model/undiscriminated-unions/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/undiscriminated-unions/src/Core/Utils.php b/seed/php-model/undiscriminated-unions/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/Utils.php +++ b/seed/php-model/undiscriminated-unions/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/undiscriminated-unions/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/undiscriminated-unions/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/undiscriminated-unions/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/undiscriminated-unions/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/undiscriminated-unions/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/undiscriminated-unions/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/undiscriminated-unions/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/undiscriminated-unions/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/TestTypeTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/undiscriminated-unions/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/undiscriminated-unions/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/unions/src/Core/JsonDeserializer.php b/seed/php-model/unions/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/unions/src/Core/JsonDeserializer.php +++ b/seed/php-model/unions/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/unions/src/Core/JsonSerializer.php b/seed/php-model/unions/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/unions/src/Core/JsonSerializer.php +++ b/seed/php-model/unions/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/unions/src/Core/SerializableType.php b/seed/php-model/unions/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/unions/src/Core/SerializableType.php +++ b/seed/php-model/unions/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/unions/src/Core/Union.php b/seed/php-model/unions/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/unions/src/Core/Union.php +++ b/seed/php-model/unions/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/unions/src/Core/Utils.php b/seed/php-model/unions/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/unions/src/Core/Utils.php +++ b/seed/php-model/unions/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/unions/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/unions/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/unions/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/unions/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/unions/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/unions/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/unions/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/unions/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/unions/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/unions/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/unions/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/unions/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/unions/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/unions/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/unions/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/unions/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/unions/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/unions/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/unions/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/unions/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/unions/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/unions/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/unions/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/unions/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/unions/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/unions/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/unions/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/unions/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/unions/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/unions/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/unions/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/unions/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/unions/tests/Seed/Core/TestTypeTest.php b/seed/php-model/unions/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/unions/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/unions/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/unions/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/unions/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/unions/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/unions/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/unknown/src/Core/JsonDeserializer.php b/seed/php-model/unknown/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/unknown/src/Core/JsonDeserializer.php +++ b/seed/php-model/unknown/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/unknown/src/Core/JsonSerializer.php b/seed/php-model/unknown/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/unknown/src/Core/JsonSerializer.php +++ b/seed/php-model/unknown/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/unknown/src/Core/SerializableType.php b/seed/php-model/unknown/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/unknown/src/Core/SerializableType.php +++ b/seed/php-model/unknown/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/unknown/src/Core/Union.php b/seed/php-model/unknown/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/unknown/src/Core/Union.php +++ b/seed/php-model/unknown/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/unknown/src/Core/Utils.php b/seed/php-model/unknown/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/unknown/src/Core/Utils.php +++ b/seed/php-model/unknown/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/unknown/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/unknown/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/unknown/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/unknown/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/unknown/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/unknown/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/unknown/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/unknown/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/unknown/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/unknown/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/unknown/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/unknown/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/unknown/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/unknown/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/unknown/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/unknown/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/unknown/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/unknown/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/unknown/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/unknown/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/unknown/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/unknown/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/unknown/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/unknown/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/unknown/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/unknown/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/unknown/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/unknown/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/unknown/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/unknown/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/unknown/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/unknown/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/unknown/tests/Seed/Core/TestTypeTest.php b/seed/php-model/unknown/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/unknown/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/unknown/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/unknown/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/unknown/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/unknown/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/unknown/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/validation/src/Core/JsonDeserializer.php b/seed/php-model/validation/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/validation/src/Core/JsonDeserializer.php +++ b/seed/php-model/validation/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/validation/src/Core/JsonSerializer.php b/seed/php-model/validation/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/validation/src/Core/JsonSerializer.php +++ b/seed/php-model/validation/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/validation/src/Core/SerializableType.php b/seed/php-model/validation/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/validation/src/Core/SerializableType.php +++ b/seed/php-model/validation/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/validation/src/Core/Union.php b/seed/php-model/validation/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/validation/src/Core/Union.php +++ b/seed/php-model/validation/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/validation/src/Core/Utils.php b/seed/php-model/validation/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/validation/src/Core/Utils.php +++ b/seed/php-model/validation/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/validation/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/validation/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/validation/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/validation/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/validation/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/validation/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/validation/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/validation/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/validation/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/validation/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/validation/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/validation/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/validation/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/validation/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/validation/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/validation/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/validation/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/validation/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/validation/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/validation/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/validation/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/validation/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/validation/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/validation/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/validation/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/validation/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/validation/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/validation/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/validation/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/validation/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/validation/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/validation/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/validation/tests/Seed/Core/TestTypeTest.php b/seed/php-model/validation/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/validation/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/validation/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/validation/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/validation/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/validation/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/validation/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/variables/src/Core/JsonDeserializer.php b/seed/php-model/variables/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/variables/src/Core/JsonDeserializer.php +++ b/seed/php-model/variables/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/variables/src/Core/JsonSerializer.php b/seed/php-model/variables/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/variables/src/Core/JsonSerializer.php +++ b/seed/php-model/variables/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/variables/src/Core/SerializableType.php b/seed/php-model/variables/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/variables/src/Core/SerializableType.php +++ b/seed/php-model/variables/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/variables/src/Core/Union.php b/seed/php-model/variables/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/variables/src/Core/Union.php +++ b/seed/php-model/variables/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/variables/src/Core/Utils.php b/seed/php-model/variables/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/variables/src/Core/Utils.php +++ b/seed/php-model/variables/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/variables/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/variables/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/variables/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/variables/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/variables/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/variables/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/variables/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/variables/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/variables/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/variables/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/variables/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/variables/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/variables/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/variables/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/variables/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/variables/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/variables/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/variables/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/variables/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/variables/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/variables/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/variables/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/variables/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/variables/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/variables/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/variables/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/variables/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/variables/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/variables/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/variables/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/variables/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/variables/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/variables/tests/Seed/Core/TestTypeTest.php b/seed/php-model/variables/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/variables/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/variables/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/variables/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/variables/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/variables/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/variables/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/version-no-default/src/Core/JsonDeserializer.php b/seed/php-model/version-no-default/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/version-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-model/version-no-default/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/version-no-default/src/Core/JsonSerializer.php b/seed/php-model/version-no-default/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/version-no-default/src/Core/JsonSerializer.php +++ b/seed/php-model/version-no-default/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/version-no-default/src/Core/SerializableType.php b/seed/php-model/version-no-default/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/version-no-default/src/Core/SerializableType.php +++ b/seed/php-model/version-no-default/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/version-no-default/src/Core/Union.php b/seed/php-model/version-no-default/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/version-no-default/src/Core/Union.php +++ b/seed/php-model/version-no-default/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/version-no-default/src/Core/Utils.php b/seed/php-model/version-no-default/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/version-no-default/src/Core/Utils.php +++ b/seed/php-model/version-no-default/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/version-no-default/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/version-no-default/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/version-no-default/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/version-no-default/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/version-no-default/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/version-no-default/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/version-no-default/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/version-no-default/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/version-no-default/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/version-no-default/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/version-no-default/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/version-no-default/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/version-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/version-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/version-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/version-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/version-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/version-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/version-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/version-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/version-no-default/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/version-no-default/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/version-no-default/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/version-no-default/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/version-no-default/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/version-no-default/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/version-no-default/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/version-no-default/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/version-no-default/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/version-no-default/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/version-no-default/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/version-no-default/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/version-no-default/tests/Seed/Core/TestTypeTest.php b/seed/php-model/version-no-default/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/version-no-default/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/version-no-default/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/version-no-default/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/version-no-default/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/version-no-default/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/version-no-default/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/version/src/Core/JsonDeserializer.php b/seed/php-model/version/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/version/src/Core/JsonDeserializer.php +++ b/seed/php-model/version/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/version/src/Core/JsonSerializer.php b/seed/php-model/version/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/version/src/Core/JsonSerializer.php +++ b/seed/php-model/version/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/version/src/Core/SerializableType.php b/seed/php-model/version/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/version/src/Core/SerializableType.php +++ b/seed/php-model/version/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/version/src/Core/Union.php b/seed/php-model/version/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/version/src/Core/Union.php +++ b/seed/php-model/version/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/version/src/Core/Utils.php b/seed/php-model/version/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/version/src/Core/Utils.php +++ b/seed/php-model/version/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/version/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/version/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/version/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/version/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/version/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/version/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/version/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/version/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/version/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/version/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/version/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/version/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/version/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/version/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/version/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/version/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/version/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/version/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/version/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/version/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/version/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/version/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/version/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/version/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/version/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/version/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/version/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/version/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/version/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/version/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/version/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/version/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/version/tests/Seed/Core/TestTypeTest.php b/seed/php-model/version/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/version/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/version/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/version/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/version/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/version/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/version/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-model/websocket/src/Core/JsonDeserializer.php b/seed/php-model/websocket/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-model/websocket/src/Core/JsonDeserializer.php +++ b/seed/php-model/websocket/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/websocket/src/Core/JsonSerializer.php b/seed/php-model/websocket/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-model/websocket/src/Core/JsonSerializer.php +++ b/seed/php-model/websocket/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-model/websocket/src/Core/SerializableType.php b/seed/php-model/websocket/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-model/websocket/src/Core/SerializableType.php +++ b/seed/php-model/websocket/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-model/websocket/src/Core/Union.php b/seed/php-model/websocket/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-model/websocket/src/Core/Union.php +++ b/seed/php-model/websocket/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-model/websocket/src/Core/Utils.php b/seed/php-model/websocket/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-model/websocket/src/Core/Utils.php +++ b/seed/php-model/websocket/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-model/websocket/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/websocket/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-model/websocket/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-model/websocket/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-model/websocket/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/websocket/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-model/websocket/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-model/websocket/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-model/websocket/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/websocket/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-model/websocket/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-model/websocket/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-model/websocket/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/websocket/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-model/websocket/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-model/websocket/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-model/websocket/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/websocket/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-model/websocket/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-model/websocket/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-model/websocket/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/websocket/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-model/websocket/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-model/websocket/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-model/websocket/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/websocket/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-model/websocket/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-model/websocket/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-model/websocket/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/websocket/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-model/websocket/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-model/websocket/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-model/websocket/tests/Seed/Core/TestTypeTest.php b/seed/php-model/websocket/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-model/websocket/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-model/websocket/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-model/websocket/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/websocket/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-model/websocket/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-model/websocket/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php b/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php b/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php +++ b/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/alias-extends/src/Core/SerializableType.php b/seed/php-sdk/alias-extends/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/alias-extends/src/Core/SerializableType.php +++ b/seed/php-sdk/alias-extends/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/alias-extends/src/Core/Union.php b/seed/php-sdk/alias-extends/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/alias-extends/src/Core/Union.php +++ b/seed/php-sdk/alias-extends/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/alias-extends/src/Core/Utils.php b/seed/php-sdk/alias-extends/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/alias-extends/src/Core/Utils.php +++ b/seed/php-sdk/alias-extends/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/alias-extends/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/alias-extends/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/alias-extends/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/alias-extends/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/alias-extends/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/alias-extends/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/alias-extends/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/alias-extends/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/alias-extends/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/alias-extends/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/alias/src/Core/JsonDeserializer.php b/seed/php-sdk/alias/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/alias/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/alias/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/alias/src/Core/JsonSerializer.php b/seed/php-sdk/alias/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/alias/src/Core/JsonSerializer.php +++ b/seed/php-sdk/alias/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/alias/src/Core/SerializableType.php b/seed/php-sdk/alias/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/alias/src/Core/SerializableType.php +++ b/seed/php-sdk/alias/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/alias/src/Core/Union.php b/seed/php-sdk/alias/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/alias/src/Core/Union.php +++ b/seed/php-sdk/alias/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/alias/src/Core/Utils.php b/seed/php-sdk/alias/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/alias/src/Core/Utils.php +++ b/seed/php-sdk/alias/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/alias/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/alias/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/alias/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/alias/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/alias/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/alias/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/alias/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/alias/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/alias/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/alias/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/alias/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/alias/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/alias/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/alias/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/alias/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/alias/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/alias/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/alias/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/alias/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/alias/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/alias/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/alias/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/alias/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/alias/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/alias/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/alias/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/alias/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/alias/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/alias/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/alias/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/alias/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/alias/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/alias/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/alias/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/alias/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/alias/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/alias/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/alias/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/alias/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/alias/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php b/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/any-auth/src/Core/JsonSerializer.php b/seed/php-sdk/any-auth/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/any-auth/src/Core/JsonSerializer.php +++ b/seed/php-sdk/any-auth/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/any-auth/src/Core/SerializableType.php b/seed/php-sdk/any-auth/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/any-auth/src/Core/SerializableType.php +++ b/seed/php-sdk/any-auth/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/any-auth/src/Core/Union.php b/seed/php-sdk/any-auth/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/any-auth/src/Core/Union.php +++ b/seed/php-sdk/any-auth/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/any-auth/src/Core/Utils.php b/seed/php-sdk/any-auth/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/any-auth/src/Core/Utils.php +++ b/seed/php-sdk/any-auth/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/any-auth/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/any-auth/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/any-auth/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/any-auth/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/any-auth/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/any-auth/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/any-auth/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/any-auth/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/any-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/any-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/any-auth/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/any-auth/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/any-auth/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/any-auth/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/any-auth/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/any-auth/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/any-auth/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/any-auth/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/any-auth/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/any-auth/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/api-wide-base-path/src/Core/JsonDeserializer.php b/seed/php-sdk/api-wide-base-path/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/api-wide-base-path/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/api-wide-base-path/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/api-wide-base-path/src/Core/JsonSerializer.php b/seed/php-sdk/api-wide-base-path/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/api-wide-base-path/src/Core/JsonSerializer.php +++ b/seed/php-sdk/api-wide-base-path/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/api-wide-base-path/src/Core/SerializableType.php b/seed/php-sdk/api-wide-base-path/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/api-wide-base-path/src/Core/SerializableType.php +++ b/seed/php-sdk/api-wide-base-path/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/api-wide-base-path/src/Core/Union.php b/seed/php-sdk/api-wide-base-path/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/api-wide-base-path/src/Core/Union.php +++ b/seed/php-sdk/api-wide-base-path/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/api-wide-base-path/src/Core/Utils.php b/seed/php-sdk/api-wide-base-path/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/api-wide-base-path/src/Core/Utils.php +++ b/seed/php-sdk/api-wide-base-path/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/audiences/src/Core/JsonDeserializer.php b/seed/php-sdk/audiences/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/audiences/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/audiences/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/audiences/src/Core/JsonSerializer.php b/seed/php-sdk/audiences/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/audiences/src/Core/JsonSerializer.php +++ b/seed/php-sdk/audiences/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/audiences/src/Core/SerializableType.php b/seed/php-sdk/audiences/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/audiences/src/Core/SerializableType.php +++ b/seed/php-sdk/audiences/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/audiences/src/Core/Union.php b/seed/php-sdk/audiences/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/audiences/src/Core/Union.php +++ b/seed/php-sdk/audiences/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/audiences/src/Core/Utils.php b/seed/php-sdk/audiences/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/audiences/src/Core/Utils.php +++ b/seed/php-sdk/audiences/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/audiences/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/audiences/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/audiences/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/audiences/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/audiences/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/audiences/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/audiences/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/audiences/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/audiences/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/audiences/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/audiences/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/audiences/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/audiences/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/audiences/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/audiences/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/audiences/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/audiences/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/audiences/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/audiences/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/audiences/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/audiences/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/audiences/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/audiences/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/audiences/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/audiences/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/audiences/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/audiences/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/audiences/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/audiences/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/audiences/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/audiences/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/audiences/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/audiences/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/audiences/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/audiences/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/audiences/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/audiences/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/audiences/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/audiences/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/audiences/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/auth-environment-variables/src/Core/JsonDeserializer.php b/seed/php-sdk/auth-environment-variables/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/auth-environment-variables/src/Core/JsonSerializer.php b/seed/php-sdk/auth-environment-variables/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/auth-environment-variables/src/Core/SerializableType.php b/seed/php-sdk/auth-environment-variables/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/SerializableType.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/auth-environment-variables/src/Core/Union.php b/seed/php-sdk/auth-environment-variables/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/Union.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/auth-environment-variables/src/Core/Utils.php b/seed/php-sdk/auth-environment-variables/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/Utils.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDeserializer.php b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonSerializer.php b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/basic-auth-environment-variables/src/Core/SerializableType.php b/seed/php-sdk/basic-auth-environment-variables/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/basic-auth-environment-variables/src/Core/SerializableType.php +++ b/seed/php-sdk/basic-auth-environment-variables/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/basic-auth-environment-variables/src/Core/Union.php b/seed/php-sdk/basic-auth-environment-variables/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/basic-auth-environment-variables/src/Core/Union.php +++ b/seed/php-sdk/basic-auth-environment-variables/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/basic-auth-environment-variables/src/Core/Utils.php b/seed/php-sdk/basic-auth-environment-variables/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/basic-auth-environment-variables/src/Core/Utils.php +++ b/seed/php-sdk/basic-auth-environment-variables/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php b/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php b/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php +++ b/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/basic-auth/src/Core/SerializableType.php b/seed/php-sdk/basic-auth/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/basic-auth/src/Core/SerializableType.php +++ b/seed/php-sdk/basic-auth/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/basic-auth/src/Core/Union.php b/seed/php-sdk/basic-auth/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/basic-auth/src/Core/Union.php +++ b/seed/php-sdk/basic-auth/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/basic-auth/src/Core/Utils.php b/seed/php-sdk/basic-auth/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/basic-auth/src/Core/Utils.php +++ b/seed/php-sdk/basic-auth/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/basic-auth/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/basic-auth/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/basic-auth/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/basic-auth/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/basic-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/basic-auth/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/basic-auth/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/basic-auth/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/basic-auth/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/basic-auth/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDeserializer.php b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonSerializer.php b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonSerializer.php +++ b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/bearer-token-environment-variable/src/Core/SerializableType.php b/seed/php-sdk/bearer-token-environment-variable/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/bearer-token-environment-variable/src/Core/SerializableType.php +++ b/seed/php-sdk/bearer-token-environment-variable/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/bearer-token-environment-variable/src/Core/Union.php b/seed/php-sdk/bearer-token-environment-variable/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/bearer-token-environment-variable/src/Core/Union.php +++ b/seed/php-sdk/bearer-token-environment-variable/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/bearer-token-environment-variable/src/Core/Utils.php b/seed/php-sdk/bearer-token-environment-variable/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/bearer-token-environment-variable/src/Core/Utils.php +++ b/seed/php-sdk/bearer-token-environment-variable/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/bytes/src/Core/JsonDeserializer.php b/seed/php-sdk/bytes/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/bytes/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/bytes/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/bytes/src/Core/JsonSerializer.php b/seed/php-sdk/bytes/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/bytes/src/Core/JsonSerializer.php +++ b/seed/php-sdk/bytes/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/bytes/src/Core/SerializableType.php b/seed/php-sdk/bytes/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/bytes/src/Core/SerializableType.php +++ b/seed/php-sdk/bytes/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/bytes/src/Core/Union.php b/seed/php-sdk/bytes/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/bytes/src/Core/Union.php +++ b/seed/php-sdk/bytes/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/bytes/src/Core/Utils.php b/seed/php-sdk/bytes/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/bytes/src/Core/Utils.php +++ b/seed/php-sdk/bytes/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/bytes/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/bytes/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/bytes/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/bytes/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/bytes/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/bytes/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/bytes/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/bytes/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/bytes/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/bytes/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/bytes/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/bytes/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/bytes/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/bytes/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/bytes/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/bytes/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/bytes/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/bytes/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/bytes/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/bytes/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/bytes/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/bytes/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/bytes/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/bytes/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/bytes/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/bytes/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/bytes/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/bytes/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/bytes/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/bytes/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/bytes/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/bytes/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/bytes/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/bytes/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/bytes/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/bytes/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/bytes/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/bytes/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/bytes/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/bytes/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/circular-references-advanced/src/Core/JsonDeserializer.php b/seed/php-sdk/circular-references-advanced/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/circular-references-advanced/src/Core/JsonSerializer.php b/seed/php-sdk/circular-references-advanced/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/JsonSerializer.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/circular-references-advanced/src/Core/SerializableType.php b/seed/php-sdk/circular-references-advanced/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/SerializableType.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/circular-references-advanced/src/Core/Union.php b/seed/php-sdk/circular-references-advanced/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/Union.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/circular-references-advanced/src/Core/Utils.php b/seed/php-sdk/circular-references-advanced/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/Utils.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php b/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/circular-references/src/Core/JsonSerializer.php b/seed/php-sdk/circular-references/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/circular-references/src/Core/JsonSerializer.php +++ b/seed/php-sdk/circular-references/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/circular-references/src/Core/SerializableType.php b/seed/php-sdk/circular-references/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/circular-references/src/Core/SerializableType.php +++ b/seed/php-sdk/circular-references/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/circular-references/src/Core/Union.php b/seed/php-sdk/circular-references/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/circular-references/src/Core/Union.php +++ b/seed/php-sdk/circular-references/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/circular-references/src/Core/Utils.php b/seed/php-sdk/circular-references/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/circular-references/src/Core/Utils.php +++ b/seed/php-sdk/circular-references/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/circular-references/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/circular-references/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/circular-references/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/circular-references/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/circular-references/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/circular-references/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/circular-references/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/circular-references/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/circular-references/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/circular-references/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/circular-references/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/circular-references/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/circular-references/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/circular-references/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/circular-references/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/circular-references/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/circular-references/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/circular-references/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/circular-references/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/circular-references/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/cross-package-type-names/src/Core/JsonDeserializer.php b/seed/php-sdk/cross-package-type-names/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/cross-package-type-names/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/cross-package-type-names/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/cross-package-type-names/src/Core/JsonSerializer.php b/seed/php-sdk/cross-package-type-names/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/cross-package-type-names/src/Core/JsonSerializer.php +++ b/seed/php-sdk/cross-package-type-names/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/cross-package-type-names/src/Core/SerializableType.php b/seed/php-sdk/cross-package-type-names/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/cross-package-type-names/src/Core/SerializableType.php +++ b/seed/php-sdk/cross-package-type-names/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/cross-package-type-names/src/Core/Union.php b/seed/php-sdk/cross-package-type-names/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/cross-package-type-names/src/Core/Union.php +++ b/seed/php-sdk/cross-package-type-names/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/cross-package-type-names/src/Core/Utils.php b/seed/php-sdk/cross-package-type-names/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/cross-package-type-names/src/Core/Utils.php +++ b/seed/php-sdk/cross-package-type-names/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php b/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php b/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php +++ b/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/custom-auth/src/Core/SerializableType.php b/seed/php-sdk/custom-auth/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/custom-auth/src/Core/SerializableType.php +++ b/seed/php-sdk/custom-auth/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/custom-auth/src/Core/Union.php b/seed/php-sdk/custom-auth/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/custom-auth/src/Core/Union.php +++ b/seed/php-sdk/custom-auth/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/custom-auth/src/Core/Utils.php b/seed/php-sdk/custom-auth/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/custom-auth/src/Core/Utils.php +++ b/seed/php-sdk/custom-auth/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/custom-auth/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/custom-auth/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/custom-auth/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/custom-auth/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/custom-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/custom-auth/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/custom-auth/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/custom-auth/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/custom-auth/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/custom-auth/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/enum/src/Core/JsonDeserializer.php b/seed/php-sdk/enum/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/enum/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/enum/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/enum/src/Core/JsonSerializer.php b/seed/php-sdk/enum/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/enum/src/Core/JsonSerializer.php +++ b/seed/php-sdk/enum/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/enum/src/Core/SerializableType.php b/seed/php-sdk/enum/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/enum/src/Core/SerializableType.php +++ b/seed/php-sdk/enum/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/enum/src/Core/Union.php b/seed/php-sdk/enum/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/enum/src/Core/Union.php +++ b/seed/php-sdk/enum/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/enum/src/Core/Utils.php b/seed/php-sdk/enum/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/enum/src/Core/Utils.php +++ b/seed/php-sdk/enum/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/enum/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/enum/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/enum/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/enum/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/enum/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/enum/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/enum/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/enum/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/enum/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/enum/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/enum/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/enum/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/enum/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/enum/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/enum/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/enum/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/enum/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/enum/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/enum/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/enum/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/enum/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/enum/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/enum/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/enum/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/enum/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/enum/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/enum/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/enum/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/enum/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/enum/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/enum/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/enum/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/enum/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/enum/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/enum/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/enum/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/enum/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/enum/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/enum/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/enum/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/error-property/src/Core/JsonDeserializer.php b/seed/php-sdk/error-property/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/error-property/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/error-property/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/error-property/src/Core/JsonSerializer.php b/seed/php-sdk/error-property/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/error-property/src/Core/JsonSerializer.php +++ b/seed/php-sdk/error-property/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/error-property/src/Core/SerializableType.php b/seed/php-sdk/error-property/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/error-property/src/Core/SerializableType.php +++ b/seed/php-sdk/error-property/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/error-property/src/Core/Union.php b/seed/php-sdk/error-property/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/error-property/src/Core/Union.php +++ b/seed/php-sdk/error-property/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/error-property/src/Core/Utils.php b/seed/php-sdk/error-property/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/error-property/src/Core/Utils.php +++ b/seed/php-sdk/error-property/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/error-property/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/error-property/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/error-property/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/error-property/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/error-property/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/error-property/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/error-property/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/error-property/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/error-property/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/error-property/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/error-property/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/error-property/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/error-property/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/error-property/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/error-property/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/error-property/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/error-property/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/error-property/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/error-property/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/error-property/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/error-property/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/error-property/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/error-property/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/error-property/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/error-property/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/error-property/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/error-property/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/error-property/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/error-property/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/error-property/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/error-property/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/error-property/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/error-property/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/error-property/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/error-property/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/error-property/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/error-property/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/error-property/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/error-property/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/error-property/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/examples/src/Core/JsonDeserializer.php b/seed/php-sdk/examples/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/examples/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/examples/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/examples/src/Core/JsonSerializer.php b/seed/php-sdk/examples/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/examples/src/Core/JsonSerializer.php +++ b/seed/php-sdk/examples/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/examples/src/Core/SerializableType.php b/seed/php-sdk/examples/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/examples/src/Core/SerializableType.php +++ b/seed/php-sdk/examples/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/examples/src/Core/Union.php b/seed/php-sdk/examples/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/examples/src/Core/Union.php +++ b/seed/php-sdk/examples/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/examples/src/Core/Utils.php b/seed/php-sdk/examples/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/examples/src/Core/Utils.php +++ b/seed/php-sdk/examples/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/examples/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/examples/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/examples/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/examples/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/examples/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/examples/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/examples/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/examples/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/examples/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/examples/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/examples/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/examples/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/examples/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/examples/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/examples/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/examples/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/examples/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/examples/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/examples/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/examples/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/examples/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/examples/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/examples/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/examples/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/examples/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/examples/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/examples/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/examples/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/examples/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/examples/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/examples/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/examples/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/examples/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/examples/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/examples/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/examples/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/examples/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/examples/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/examples/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/examples/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php b/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php b/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/exhaustive/src/Core/SerializableType.php b/seed/php-sdk/exhaustive/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/exhaustive/src/Core/SerializableType.php +++ b/seed/php-sdk/exhaustive/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/exhaustive/src/Core/Union.php b/seed/php-sdk/exhaustive/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/exhaustive/src/Core/Union.php +++ b/seed/php-sdk/exhaustive/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/exhaustive/src/Core/Utils.php b/seed/php-sdk/exhaustive/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/exhaustive/src/Core/Utils.php +++ b/seed/php-sdk/exhaustive/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/exhaustive/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/exhaustive/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/exhaustive/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/exhaustive/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/exhaustive/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/exhaustive/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/exhaustive/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/exhaustive/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/extends/src/Core/JsonDeserializer.php b/seed/php-sdk/extends/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/extends/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/extends/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/extends/src/Core/JsonSerializer.php b/seed/php-sdk/extends/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/extends/src/Core/JsonSerializer.php +++ b/seed/php-sdk/extends/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/extends/src/Core/SerializableType.php b/seed/php-sdk/extends/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/extends/src/Core/SerializableType.php +++ b/seed/php-sdk/extends/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/extends/src/Core/Union.php b/seed/php-sdk/extends/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/extends/src/Core/Union.php +++ b/seed/php-sdk/extends/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/extends/src/Core/Utils.php b/seed/php-sdk/extends/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/extends/src/Core/Utils.php +++ b/seed/php-sdk/extends/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/extends/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/extends/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/extends/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/extends/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/extends/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/extends/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/extends/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/extends/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/extends/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/extends/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/extends/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/extends/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/extends/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/extends/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/extends/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/extends/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/extends/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/extends/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/extends/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/extends/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/extends/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/extends/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/extends/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/extends/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/extends/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/extends/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/extends/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/extends/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/extends/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/extends/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/extends/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/extends/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/extends/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/extends/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/extends/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/extends/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/extends/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/extends/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/extends/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/extends/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php b/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php b/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php +++ b/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/extra-properties/src/Core/SerializableType.php b/seed/php-sdk/extra-properties/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/extra-properties/src/Core/SerializableType.php +++ b/seed/php-sdk/extra-properties/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/extra-properties/src/Core/Union.php b/seed/php-sdk/extra-properties/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/extra-properties/src/Core/Union.php +++ b/seed/php-sdk/extra-properties/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/extra-properties/src/Core/Utils.php b/seed/php-sdk/extra-properties/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/extra-properties/src/Core/Utils.php +++ b/seed/php-sdk/extra-properties/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/extra-properties/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/extra-properties/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/extra-properties/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/extra-properties/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/extra-properties/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/extra-properties/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/extra-properties/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/extra-properties/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/extra-properties/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/extra-properties/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/file-download/src/Core/JsonDeserializer.php b/seed/php-sdk/file-download/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/file-download/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/file-download/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/file-download/src/Core/JsonSerializer.php b/seed/php-sdk/file-download/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/file-download/src/Core/JsonSerializer.php +++ b/seed/php-sdk/file-download/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/file-download/src/Core/SerializableType.php b/seed/php-sdk/file-download/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/file-download/src/Core/SerializableType.php +++ b/seed/php-sdk/file-download/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/file-download/src/Core/Union.php b/seed/php-sdk/file-download/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/file-download/src/Core/Union.php +++ b/seed/php-sdk/file-download/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/file-download/src/Core/Utils.php b/seed/php-sdk/file-download/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/file-download/src/Core/Utils.php +++ b/seed/php-sdk/file-download/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/file-download/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/file-download/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/file-download/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/file-download/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/file-download/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/file-download/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/file-download/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/file-download/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/file-download/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/file-download/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/file-download/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/file-download/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/file-download/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/file-download/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/file-download/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/file-download/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/file-download/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/file-download/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/file-download/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/file-download/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/file-download/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/file-download/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/file-download/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/file-download/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/file-download/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/file-download/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/file-download/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/file-download/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/file-download/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/file-download/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/file-download/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/file-download/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/file-download/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/file-download/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/file-download/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/file-download/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/file-download/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/file-download/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/file-download/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/file-download/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php b/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/file-upload/src/Core/JsonSerializer.php b/seed/php-sdk/file-upload/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/file-upload/src/Core/JsonSerializer.php +++ b/seed/php-sdk/file-upload/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/file-upload/src/Core/SerializableType.php b/seed/php-sdk/file-upload/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/file-upload/src/Core/SerializableType.php +++ b/seed/php-sdk/file-upload/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/file-upload/src/Core/Union.php b/seed/php-sdk/file-upload/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/file-upload/src/Core/Union.php +++ b/seed/php-sdk/file-upload/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/file-upload/src/Core/Utils.php b/seed/php-sdk/file-upload/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/file-upload/src/Core/Utils.php +++ b/seed/php-sdk/file-upload/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/file-upload/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/file-upload/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/file-upload/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/file-upload/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/file-upload/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/file-upload/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/file-upload/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/file-upload/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/file-upload/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/file-upload/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/file-upload/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/file-upload/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/file-upload/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/file-upload/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/file-upload/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/file-upload/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/file-upload/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/file-upload/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/file-upload/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/file-upload/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/folders/src/Core/JsonDeserializer.php b/seed/php-sdk/folders/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/folders/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/folders/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/folders/src/Core/JsonSerializer.php b/seed/php-sdk/folders/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/folders/src/Core/JsonSerializer.php +++ b/seed/php-sdk/folders/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/folders/src/Core/SerializableType.php b/seed/php-sdk/folders/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/folders/src/Core/SerializableType.php +++ b/seed/php-sdk/folders/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/folders/src/Core/Union.php b/seed/php-sdk/folders/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/folders/src/Core/Union.php +++ b/seed/php-sdk/folders/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/folders/src/Core/Utils.php b/seed/php-sdk/folders/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/folders/src/Core/Utils.php +++ b/seed/php-sdk/folders/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/folders/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/folders/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/folders/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/folders/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/folders/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/folders/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/folders/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/folders/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/folders/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/folders/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/folders/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/folders/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/folders/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/folders/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/folders/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/folders/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/folders/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/folders/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/folders/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/folders/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/folders/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/folders/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/folders/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/folders/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/folders/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/folders/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/folders/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/folders/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/folders/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/folders/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/folders/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/folders/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/folders/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/folders/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/folders/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/folders/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/folders/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/folders/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/folders/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/folders/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDeserializer.php b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonSerializer.php b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Core/SerializableType.php b/seed/php-sdk/grpc-proto-exhaustive/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/SerializableType.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Core/Union.php b/seed/php-sdk/grpc-proto-exhaustive/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/Union.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Core/Utils.php b/seed/php-sdk/grpc-proto-exhaustive/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/Utils.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php b/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php b/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php +++ b/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/grpc-proto/src/Core/SerializableType.php b/seed/php-sdk/grpc-proto/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/grpc-proto/src/Core/SerializableType.php +++ b/seed/php-sdk/grpc-proto/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/grpc-proto/src/Core/Union.php b/seed/php-sdk/grpc-proto/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/grpc-proto/src/Core/Union.php +++ b/seed/php-sdk/grpc-proto/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/grpc-proto/src/Core/Utils.php b/seed/php-sdk/grpc-proto/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/grpc-proto/src/Core/Utils.php +++ b/seed/php-sdk/grpc-proto/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/grpc-proto/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/grpc-proto/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/grpc-proto/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/grpc-proto/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/grpc-proto/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/grpc-proto/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/grpc-proto/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/grpc-proto/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/grpc-proto/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/grpc-proto/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php b/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php b/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php +++ b/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php b/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php +++ b/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/idempotency-headers/src/Core/Union.php b/seed/php-sdk/idempotency-headers/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/Union.php +++ b/seed/php-sdk/idempotency-headers/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/idempotency-headers/src/Core/Utils.php b/seed/php-sdk/idempotency-headers/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/Utils.php +++ b/seed/php-sdk/idempotency-headers/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/idempotency-headers/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/idempotency-headers/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/idempotency-headers/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/idempotency-headers/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/idempotency-headers/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/idempotency-headers/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/idempotency-headers/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/idempotency-headers/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/idempotency-headers/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/idempotency-headers/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/imdb/src/Core/JsonDeserializer.php b/seed/php-sdk/imdb/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/imdb/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/imdb/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/imdb/src/Core/JsonSerializer.php b/seed/php-sdk/imdb/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/imdb/src/Core/JsonSerializer.php +++ b/seed/php-sdk/imdb/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/imdb/src/Core/SerializableType.php b/seed/php-sdk/imdb/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/imdb/src/Core/SerializableType.php +++ b/seed/php-sdk/imdb/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/imdb/src/Core/Union.php b/seed/php-sdk/imdb/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/imdb/src/Core/Union.php +++ b/seed/php-sdk/imdb/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/imdb/src/Core/Utils.php b/seed/php-sdk/imdb/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/imdb/src/Core/Utils.php +++ b/seed/php-sdk/imdb/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/imdb/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/imdb/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/imdb/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/imdb/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/imdb/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/imdb/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/imdb/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/imdb/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/imdb/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/imdb/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/imdb/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/imdb/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/imdb/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/imdb/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/imdb/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/imdb/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/imdb/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/imdb/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/imdb/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/imdb/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/imdb/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/imdb/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/imdb/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/imdb/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/imdb/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/imdb/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/imdb/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/imdb/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/imdb/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/imdb/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/imdb/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/imdb/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/imdb/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/imdb/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/imdb/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/imdb/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/imdb/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/imdb/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/imdb/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/imdb/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/literal/src/Core/JsonDeserializer.php b/seed/php-sdk/literal/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/literal/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/literal/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/literal/src/Core/JsonSerializer.php b/seed/php-sdk/literal/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/literal/src/Core/JsonSerializer.php +++ b/seed/php-sdk/literal/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/literal/src/Core/SerializableType.php b/seed/php-sdk/literal/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/literal/src/Core/SerializableType.php +++ b/seed/php-sdk/literal/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/literal/src/Core/Union.php b/seed/php-sdk/literal/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/literal/src/Core/Union.php +++ b/seed/php-sdk/literal/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/literal/src/Core/Utils.php b/seed/php-sdk/literal/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/literal/src/Core/Utils.php +++ b/seed/php-sdk/literal/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/literal/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/literal/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/literal/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/literal/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/literal/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/literal/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/literal/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/literal/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/literal/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/literal/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/literal/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/literal/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/literal/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/literal/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/literal/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/literal/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/literal/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/literal/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/literal/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/literal/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/literal/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/literal/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/literal/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/literal/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/literal/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/literal/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/literal/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/literal/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/literal/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/literal/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/literal/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/literal/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/literal/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/literal/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/literal/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/literal/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/literal/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/literal/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/literal/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/literal/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php b/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php b/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php +++ b/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/mixed-case/src/Core/SerializableType.php b/seed/php-sdk/mixed-case/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/mixed-case/src/Core/SerializableType.php +++ b/seed/php-sdk/mixed-case/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/mixed-case/src/Core/Union.php b/seed/php-sdk/mixed-case/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/mixed-case/src/Core/Union.php +++ b/seed/php-sdk/mixed-case/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/mixed-case/src/Core/Utils.php b/seed/php-sdk/mixed-case/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/mixed-case/src/Core/Utils.php +++ b/seed/php-sdk/mixed-case/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/mixed-case/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/mixed-case/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/mixed-case/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/mixed-case/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/mixed-case/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/mixed-case/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/mixed-case/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/mixed-case/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/mixed-case/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/mixed-case/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/mixed-file-directory/src/Core/JsonDeserializer.php b/seed/php-sdk/mixed-file-directory/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/mixed-file-directory/src/Core/JsonSerializer.php b/seed/php-sdk/mixed-file-directory/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/JsonSerializer.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/mixed-file-directory/src/Core/SerializableType.php b/seed/php-sdk/mixed-file-directory/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/SerializableType.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/mixed-file-directory/src/Core/Union.php b/seed/php-sdk/mixed-file-directory/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/Union.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/mixed-file-directory/src/Core/Utils.php b/seed/php-sdk/mixed-file-directory/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/Utils.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/multi-line-docs/src/Core/JsonDeserializer.php b/seed/php-sdk/multi-line-docs/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/multi-line-docs/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/multi-line-docs/src/Core/JsonSerializer.php b/seed/php-sdk/multi-line-docs/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/JsonSerializer.php +++ b/seed/php-sdk/multi-line-docs/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/multi-line-docs/src/Core/SerializableType.php b/seed/php-sdk/multi-line-docs/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/SerializableType.php +++ b/seed/php-sdk/multi-line-docs/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/multi-line-docs/src/Core/Union.php b/seed/php-sdk/multi-line-docs/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/Union.php +++ b/seed/php-sdk/multi-line-docs/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/multi-line-docs/src/Core/Utils.php b/seed/php-sdk/multi-line-docs/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/Utils.php +++ b/seed/php-sdk/multi-line-docs/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/multi-line-docs/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/multi-line-docs/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/multi-line-docs/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/multi-line-docs/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/multi-line-docs/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/multi-line-docs/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/multi-line-docs/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/multi-line-docs/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/multi-line-docs/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/multi-line-docs/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php b/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/no-environment/src/Core/JsonSerializer.php b/seed/php-sdk/no-environment/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/no-environment/src/Core/JsonSerializer.php +++ b/seed/php-sdk/no-environment/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/no-environment/src/Core/SerializableType.php b/seed/php-sdk/no-environment/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/no-environment/src/Core/SerializableType.php +++ b/seed/php-sdk/no-environment/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/no-environment/src/Core/Union.php b/seed/php-sdk/no-environment/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/no-environment/src/Core/Union.php +++ b/seed/php-sdk/no-environment/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/no-environment/src/Core/Utils.php b/seed/php-sdk/no-environment/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/no-environment/src/Core/Utils.php +++ b/seed/php-sdk/no-environment/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/no-environment/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/no-environment/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/no-environment/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/no-environment/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/no-environment/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/no-environment/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/no-environment/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/no-environment/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/no-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/no-environment/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/no-environment/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/no-environment/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/no-environment/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/no-environment/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/no-environment/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/no-environment/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/no-environment/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/no-environment/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/no-environment/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/no-environment/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDeserializer.php b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonSerializer.php b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonSerializer.php +++ b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/oauth-client-credentials-default/src/Core/SerializableType.php b/seed/php-sdk/oauth-client-credentials-default/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/oauth-client-credentials-default/src/Core/SerializableType.php +++ b/seed/php-sdk/oauth-client-credentials-default/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/oauth-client-credentials-default/src/Core/Union.php b/seed/php-sdk/oauth-client-credentials-default/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/oauth-client-credentials-default/src/Core/Union.php +++ b/seed/php-sdk/oauth-client-credentials-default/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/oauth-client-credentials-default/src/Core/Utils.php b/seed/php-sdk/oauth-client-credentials-default/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/oauth-client-credentials-default/src/Core/Utils.php +++ b/seed/php-sdk/oauth-client-credentials-default/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/SerializableType.php b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/SerializableType.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Union.php b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Union.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Utils.php b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Utils.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/SerializableType.php b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/SerializableType.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Union.php b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Union.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Utils.php b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Utils.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials/src/Core/JsonDeserializer.php b/seed/php-sdk/oauth-client-credentials/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/oauth-client-credentials/src/Core/JsonSerializer.php b/seed/php-sdk/oauth-client-credentials/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/JsonSerializer.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/oauth-client-credentials/src/Core/SerializableType.php b/seed/php-sdk/oauth-client-credentials/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/SerializableType.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/oauth-client-credentials/src/Core/Union.php b/seed/php-sdk/oauth-client-credentials/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/Union.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/oauth-client-credentials/src/Core/Utils.php b/seed/php-sdk/oauth-client-credentials/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/Utils.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/object/src/Core/JsonDeserializer.php b/seed/php-sdk/object/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/object/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/object/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/object/src/Core/JsonSerializer.php b/seed/php-sdk/object/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/object/src/Core/JsonSerializer.php +++ b/seed/php-sdk/object/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/object/src/Core/SerializableType.php b/seed/php-sdk/object/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/object/src/Core/SerializableType.php +++ b/seed/php-sdk/object/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/object/src/Core/Union.php b/seed/php-sdk/object/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/object/src/Core/Union.php +++ b/seed/php-sdk/object/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/object/src/Core/Utils.php b/seed/php-sdk/object/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/object/src/Core/Utils.php +++ b/seed/php-sdk/object/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/object/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/object/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/object/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/object/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/object/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/object/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/object/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/object/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/object/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/object/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/object/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/object/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/object/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/object/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/object/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/object/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/object/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/object/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/object/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/object/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/object/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/object/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/object/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/object/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/object/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/object/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/object/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/object/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/object/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/object/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/object/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/object/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/object/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/object/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/object/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/object/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/object/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/object/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/object/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/object/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/objects-with-imports/src/Core/JsonDeserializer.php b/seed/php-sdk/objects-with-imports/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/objects-with-imports/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/objects-with-imports/src/Core/JsonSerializer.php b/seed/php-sdk/objects-with-imports/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/JsonSerializer.php +++ b/seed/php-sdk/objects-with-imports/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/objects-with-imports/src/Core/SerializableType.php b/seed/php-sdk/objects-with-imports/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/SerializableType.php +++ b/seed/php-sdk/objects-with-imports/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/objects-with-imports/src/Core/Union.php b/seed/php-sdk/objects-with-imports/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/Union.php +++ b/seed/php-sdk/objects-with-imports/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/objects-with-imports/src/Core/Utils.php b/seed/php-sdk/objects-with-imports/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/Utils.php +++ b/seed/php-sdk/objects-with-imports/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/objects-with-imports/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/objects-with-imports/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/objects-with-imports/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/objects-with-imports/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/objects-with-imports/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/objects-with-imports/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/objects-with-imports/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/objects-with-imports/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/objects-with-imports/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/objects-with-imports/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/optional/src/Core/JsonDeserializer.php b/seed/php-sdk/optional/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/optional/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/optional/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/optional/src/Core/JsonSerializer.php b/seed/php-sdk/optional/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/optional/src/Core/JsonSerializer.php +++ b/seed/php-sdk/optional/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/optional/src/Core/SerializableType.php b/seed/php-sdk/optional/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/optional/src/Core/SerializableType.php +++ b/seed/php-sdk/optional/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/optional/src/Core/Union.php b/seed/php-sdk/optional/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/optional/src/Core/Union.php +++ b/seed/php-sdk/optional/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/optional/src/Core/Utils.php b/seed/php-sdk/optional/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/optional/src/Core/Utils.php +++ b/seed/php-sdk/optional/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/optional/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/optional/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/optional/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/optional/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/optional/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/optional/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/optional/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/optional/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/optional/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/optional/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/optional/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/optional/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/optional/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/optional/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/optional/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/optional/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/optional/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/optional/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/optional/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/optional/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/optional/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/optional/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/optional/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/optional/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/optional/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/optional/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/optional/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/optional/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/optional/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/optional/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/optional/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/optional/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/optional/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/optional/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/optional/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/optional/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/optional/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/optional/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/optional/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/optional/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php b/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/package-yml/src/Core/JsonSerializer.php b/seed/php-sdk/package-yml/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/package-yml/src/Core/JsonSerializer.php +++ b/seed/php-sdk/package-yml/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/package-yml/src/Core/SerializableType.php b/seed/php-sdk/package-yml/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/package-yml/src/Core/SerializableType.php +++ b/seed/php-sdk/package-yml/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/package-yml/src/Core/Union.php b/seed/php-sdk/package-yml/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/package-yml/src/Core/Union.php +++ b/seed/php-sdk/package-yml/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/package-yml/src/Core/Utils.php b/seed/php-sdk/package-yml/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/package-yml/src/Core/Utils.php +++ b/seed/php-sdk/package-yml/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/package-yml/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/package-yml/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/package-yml/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/package-yml/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/package-yml/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/package-yml/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/package-yml/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/package-yml/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/package-yml/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/package-yml/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/package-yml/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/package-yml/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/package-yml/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/package-yml/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/package-yml/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/package-yml/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/package-yml/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/package-yml/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/package-yml/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/package-yml/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/pagination/src/Core/JsonDeserializer.php b/seed/php-sdk/pagination/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/pagination/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/pagination/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/pagination/src/Core/JsonSerializer.php b/seed/php-sdk/pagination/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/pagination/src/Core/JsonSerializer.php +++ b/seed/php-sdk/pagination/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/pagination/src/Core/SerializableType.php b/seed/php-sdk/pagination/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/pagination/src/Core/SerializableType.php +++ b/seed/php-sdk/pagination/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/pagination/src/Core/Union.php b/seed/php-sdk/pagination/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/pagination/src/Core/Union.php +++ b/seed/php-sdk/pagination/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/pagination/src/Core/Utils.php b/seed/php-sdk/pagination/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/pagination/src/Core/Utils.php +++ b/seed/php-sdk/pagination/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/pagination/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/pagination/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/pagination/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/pagination/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/pagination/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/pagination/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/pagination/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/pagination/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/pagination/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/pagination/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/pagination/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/pagination/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/pagination/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/pagination/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/pagination/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/pagination/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/pagination/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/pagination/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/pagination/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/pagination/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/pagination/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/pagination/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/pagination/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/pagination/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/pagination/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/pagination/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/pagination/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/pagination/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/pagination/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/pagination/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/pagination/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/pagination/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/pagination/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/pagination/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/pagination/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/pagination/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/pagination/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/pagination/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/pagination/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/pagination/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php b/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/plain-text/src/Core/JsonSerializer.php b/seed/php-sdk/plain-text/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/plain-text/src/Core/JsonSerializer.php +++ b/seed/php-sdk/plain-text/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/plain-text/src/Core/SerializableType.php b/seed/php-sdk/plain-text/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/plain-text/src/Core/SerializableType.php +++ b/seed/php-sdk/plain-text/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/plain-text/src/Core/Union.php b/seed/php-sdk/plain-text/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/plain-text/src/Core/Union.php +++ b/seed/php-sdk/plain-text/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/plain-text/src/Core/Utils.php b/seed/php-sdk/plain-text/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/plain-text/src/Core/Utils.php +++ b/seed/php-sdk/plain-text/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/plain-text/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/plain-text/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/plain-text/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/plain-text/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/plain-text/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/plain-text/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/plain-text/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/plain-text/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/plain-text/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/plain-text/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/plain-text/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/plain-text/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/plain-text/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/plain-text/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/plain-text/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/plain-text/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/plain-text/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/plain-text/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/plain-text/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/plain-text/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php b/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php b/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php +++ b/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/query-parameters/src/Core/SerializableType.php b/seed/php-sdk/query-parameters/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/query-parameters/src/Core/SerializableType.php +++ b/seed/php-sdk/query-parameters/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/query-parameters/src/Core/Union.php b/seed/php-sdk/query-parameters/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/query-parameters/src/Core/Union.php +++ b/seed/php-sdk/query-parameters/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/query-parameters/src/Core/Utils.php b/seed/php-sdk/query-parameters/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/query-parameters/src/Core/Utils.php +++ b/seed/php-sdk/query-parameters/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/query-parameters/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/query-parameters/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/query-parameters/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/query-parameters/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/query-parameters/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/query-parameters/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/query-parameters/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/query-parameters/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/query-parameters/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/query-parameters/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php b/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php b/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php +++ b/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php b/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php +++ b/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/reserved-keywords/src/Core/Union.php b/seed/php-sdk/reserved-keywords/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/Union.php +++ b/seed/php-sdk/reserved-keywords/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/reserved-keywords/src/Core/Utils.php b/seed/php-sdk/reserved-keywords/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/Utils.php +++ b/seed/php-sdk/reserved-keywords/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/reserved-keywords/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/reserved-keywords/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/reserved-keywords/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/reserved-keywords/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/reserved-keywords/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/reserved-keywords/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/reserved-keywords/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/reserved-keywords/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/reserved-keywords/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/reserved-keywords/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/response-property/src/Core/JsonDeserializer.php b/seed/php-sdk/response-property/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/response-property/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/response-property/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/response-property/src/Core/JsonSerializer.php b/seed/php-sdk/response-property/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/response-property/src/Core/JsonSerializer.php +++ b/seed/php-sdk/response-property/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/response-property/src/Core/SerializableType.php b/seed/php-sdk/response-property/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/response-property/src/Core/SerializableType.php +++ b/seed/php-sdk/response-property/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/response-property/src/Core/Union.php b/seed/php-sdk/response-property/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/response-property/src/Core/Union.php +++ b/seed/php-sdk/response-property/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/response-property/src/Core/Utils.php b/seed/php-sdk/response-property/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/response-property/src/Core/Utils.php +++ b/seed/php-sdk/response-property/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/response-property/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/response-property/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/response-property/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/response-property/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/response-property/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/response-property/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/response-property/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/response-property/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/response-property/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/response-property/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/response-property/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/response-property/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/response-property/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/response-property/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/response-property/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/response-property/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/response-property/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/response-property/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/response-property/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/response-property/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/response-property/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/response-property/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/response-property/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/response-property/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/response-property/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/response-property/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/response-property/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/response-property/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/response-property/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/response-property/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/response-property/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/response-property/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/response-property/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/response-property/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/response-property/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/response-property/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/response-property/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/response-property/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/response-property/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/response-property/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php b/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php b/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php +++ b/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/simple-fhir/src/Core/SerializableType.php b/seed/php-sdk/simple-fhir/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/simple-fhir/src/Core/SerializableType.php +++ b/seed/php-sdk/simple-fhir/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/simple-fhir/src/Core/Union.php b/seed/php-sdk/simple-fhir/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/simple-fhir/src/Core/Union.php +++ b/seed/php-sdk/simple-fhir/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/simple-fhir/src/Core/Utils.php b/seed/php-sdk/simple-fhir/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/simple-fhir/src/Core/Utils.php +++ b/seed/php-sdk/simple-fhir/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/simple-fhir/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/simple-fhir/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/simple-fhir/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/simple-fhir/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/simple-fhir/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/simple-fhir/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/simple-fhir/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/simple-fhir/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/simple-fhir/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/simple-fhir/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/single-url-environment-default/src/Core/JsonDeserializer.php b/seed/php-sdk/single-url-environment-default/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/single-url-environment-default/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/single-url-environment-default/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/single-url-environment-default/src/Core/JsonSerializer.php b/seed/php-sdk/single-url-environment-default/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/single-url-environment-default/src/Core/JsonSerializer.php +++ b/seed/php-sdk/single-url-environment-default/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/single-url-environment-default/src/Core/SerializableType.php b/seed/php-sdk/single-url-environment-default/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/single-url-environment-default/src/Core/SerializableType.php +++ b/seed/php-sdk/single-url-environment-default/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/single-url-environment-default/src/Core/Union.php b/seed/php-sdk/single-url-environment-default/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/single-url-environment-default/src/Core/Union.php +++ b/seed/php-sdk/single-url-environment-default/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/single-url-environment-default/src/Core/Utils.php b/seed/php-sdk/single-url-environment-default/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/single-url-environment-default/src/Core/Utils.php +++ b/seed/php-sdk/single-url-environment-default/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDeserializer.php b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonSerializer.php b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonSerializer.php +++ b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/single-url-environment-no-default/src/Core/SerializableType.php b/seed/php-sdk/single-url-environment-no-default/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/single-url-environment-no-default/src/Core/SerializableType.php +++ b/seed/php-sdk/single-url-environment-no-default/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/single-url-environment-no-default/src/Core/Union.php b/seed/php-sdk/single-url-environment-no-default/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/single-url-environment-no-default/src/Core/Union.php +++ b/seed/php-sdk/single-url-environment-no-default/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/single-url-environment-no-default/src/Core/Utils.php b/seed/php-sdk/single-url-environment-no-default/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/single-url-environment-no-default/src/Core/Utils.php +++ b/seed/php-sdk/single-url-environment-no-default/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php b/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php b/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php +++ b/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php b/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php +++ b/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/streaming-parameter/src/Core/Union.php b/seed/php-sdk/streaming-parameter/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/Union.php +++ b/seed/php-sdk/streaming-parameter/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/streaming-parameter/src/Core/Utils.php b/seed/php-sdk/streaming-parameter/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/Utils.php +++ b/seed/php-sdk/streaming-parameter/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/streaming-parameter/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/streaming-parameter/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/streaming-parameter/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/streaming-parameter/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/streaming-parameter/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/streaming-parameter/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/streaming-parameter/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/streaming-parameter/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/streaming-parameter/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/streaming-parameter/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/streaming/src/Core/JsonDeserializer.php b/seed/php-sdk/streaming/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/streaming/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/streaming/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/streaming/src/Core/JsonSerializer.php b/seed/php-sdk/streaming/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/streaming/src/Core/JsonSerializer.php +++ b/seed/php-sdk/streaming/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/streaming/src/Core/SerializableType.php b/seed/php-sdk/streaming/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/streaming/src/Core/SerializableType.php +++ b/seed/php-sdk/streaming/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/streaming/src/Core/Union.php b/seed/php-sdk/streaming/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/streaming/src/Core/Union.php +++ b/seed/php-sdk/streaming/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/streaming/src/Core/Utils.php b/seed/php-sdk/streaming/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/streaming/src/Core/Utils.php +++ b/seed/php-sdk/streaming/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/streaming/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/streaming/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/streaming/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/streaming/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/streaming/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/streaming/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/streaming/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/streaming/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/streaming/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/streaming/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/streaming/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/streaming/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/streaming/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/streaming/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/streaming/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/streaming/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/streaming/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/streaming/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/streaming/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/streaming/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/streaming/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/streaming/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/streaming/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/streaming/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/streaming/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/streaming/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/streaming/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/streaming/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/streaming/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/streaming/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/streaming/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/streaming/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/streaming/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/streaming/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/streaming/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/streaming/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/streaming/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/streaming/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/streaming/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/streaming/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/trace/src/Core/JsonDeserializer.php b/seed/php-sdk/trace/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/trace/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/trace/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/trace/src/Core/JsonSerializer.php b/seed/php-sdk/trace/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/trace/src/Core/JsonSerializer.php +++ b/seed/php-sdk/trace/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/trace/src/Core/SerializableType.php b/seed/php-sdk/trace/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/trace/src/Core/SerializableType.php +++ b/seed/php-sdk/trace/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/trace/src/Core/Union.php b/seed/php-sdk/trace/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/trace/src/Core/Union.php +++ b/seed/php-sdk/trace/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/trace/src/Core/Utils.php b/seed/php-sdk/trace/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/trace/src/Core/Utils.php +++ b/seed/php-sdk/trace/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/trace/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/trace/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/trace/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/trace/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/trace/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/trace/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/trace/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/trace/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/trace/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/trace/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/trace/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/trace/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/trace/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/trace/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/trace/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/trace/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/trace/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/trace/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/trace/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/trace/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/trace/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/trace/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/trace/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/trace/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/trace/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/trace/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/trace/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/trace/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/trace/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/trace/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/trace/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/trace/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/trace/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/trace/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/trace/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/trace/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/trace/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/trace/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/trace/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/trace/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php b/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php b/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php b/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/Union.php b/seed/php-sdk/undiscriminated-unions/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/Union.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/Utils.php b/seed/php-sdk/undiscriminated-unions/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/Utils.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/unions/src/Core/JsonDeserializer.php b/seed/php-sdk/unions/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/unions/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/unions/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/unions/src/Core/JsonSerializer.php b/seed/php-sdk/unions/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/unions/src/Core/JsonSerializer.php +++ b/seed/php-sdk/unions/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/unions/src/Core/SerializableType.php b/seed/php-sdk/unions/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/unions/src/Core/SerializableType.php +++ b/seed/php-sdk/unions/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/unions/src/Core/Union.php b/seed/php-sdk/unions/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/unions/src/Core/Union.php +++ b/seed/php-sdk/unions/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/unions/src/Core/Utils.php b/seed/php-sdk/unions/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/unions/src/Core/Utils.php +++ b/seed/php-sdk/unions/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/unions/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/unions/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/unions/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/unions/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/unions/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/unions/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/unions/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/unions/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/unions/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/unions/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/unions/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/unions/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/unions/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/unions/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/unions/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/unions/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/unions/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/unions/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/unions/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/unions/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/unions/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/unions/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/unions/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/unions/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/unions/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/unions/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/unions/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/unions/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/unions/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/unions/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/unions/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/unions/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/unions/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/unions/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/unions/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/unions/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/unions/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/unions/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/unions/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/unions/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/unknown/src/Core/JsonDeserializer.php b/seed/php-sdk/unknown/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/unknown/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/unknown/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/unknown/src/Core/JsonSerializer.php b/seed/php-sdk/unknown/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/unknown/src/Core/JsonSerializer.php +++ b/seed/php-sdk/unknown/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/unknown/src/Core/SerializableType.php b/seed/php-sdk/unknown/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/unknown/src/Core/SerializableType.php +++ b/seed/php-sdk/unknown/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/unknown/src/Core/Union.php b/seed/php-sdk/unknown/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/unknown/src/Core/Union.php +++ b/seed/php-sdk/unknown/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/unknown/src/Core/Utils.php b/seed/php-sdk/unknown/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/unknown/src/Core/Utils.php +++ b/seed/php-sdk/unknown/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/unknown/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/unknown/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/unknown/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/unknown/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/unknown/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/unknown/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/unknown/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/unknown/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/unknown/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/unknown/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/unknown/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/unknown/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/unknown/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/unknown/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/unknown/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/unknown/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/unknown/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/unknown/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/unknown/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/unknown/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/unknown/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/unknown/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/unknown/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/unknown/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/unknown/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/unknown/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/unknown/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/unknown/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/unknown/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/unknown/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/unknown/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/unknown/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/unknown/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/unknown/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/unknown/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/unknown/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/unknown/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/unknown/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/unknown/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/unknown/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/validation/src/Core/JsonDeserializer.php b/seed/php-sdk/validation/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/validation/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/validation/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/validation/src/Core/JsonSerializer.php b/seed/php-sdk/validation/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/validation/src/Core/JsonSerializer.php +++ b/seed/php-sdk/validation/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/validation/src/Core/SerializableType.php b/seed/php-sdk/validation/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/validation/src/Core/SerializableType.php +++ b/seed/php-sdk/validation/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/validation/src/Core/Union.php b/seed/php-sdk/validation/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/validation/src/Core/Union.php +++ b/seed/php-sdk/validation/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/validation/src/Core/Utils.php b/seed/php-sdk/validation/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/validation/src/Core/Utils.php +++ b/seed/php-sdk/validation/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/validation/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/validation/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/validation/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/validation/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/validation/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/validation/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/validation/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/validation/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/validation/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/validation/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/validation/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/validation/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/validation/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/validation/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/validation/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/validation/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/validation/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/validation/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/validation/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/validation/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/validation/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/validation/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/validation/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/validation/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/validation/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/validation/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/validation/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/validation/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/validation/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/validation/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/validation/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/validation/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/validation/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/validation/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/validation/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/validation/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/validation/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/validation/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/validation/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/validation/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/variables/src/Core/JsonDeserializer.php b/seed/php-sdk/variables/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/variables/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/variables/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/variables/src/Core/JsonSerializer.php b/seed/php-sdk/variables/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/variables/src/Core/JsonSerializer.php +++ b/seed/php-sdk/variables/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/variables/src/Core/SerializableType.php b/seed/php-sdk/variables/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/variables/src/Core/SerializableType.php +++ b/seed/php-sdk/variables/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/variables/src/Core/Union.php b/seed/php-sdk/variables/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/variables/src/Core/Union.php +++ b/seed/php-sdk/variables/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/variables/src/Core/Utils.php b/seed/php-sdk/variables/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/variables/src/Core/Utils.php +++ b/seed/php-sdk/variables/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/variables/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/variables/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/variables/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/variables/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/variables/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/variables/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/variables/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/variables/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/variables/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/variables/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/variables/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/variables/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/variables/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/variables/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/variables/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/variables/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/variables/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/variables/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/variables/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/variables/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/variables/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/variables/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/variables/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/variables/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/variables/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/variables/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/variables/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/variables/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/variables/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/variables/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/variables/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/variables/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/variables/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/variables/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/variables/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/variables/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/variables/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/variables/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/variables/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/variables/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/version-no-default/src/Core/JsonDeserializer.php b/seed/php-sdk/version-no-default/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/version-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/version-no-default/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/version-no-default/src/Core/JsonSerializer.php b/seed/php-sdk/version-no-default/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/version-no-default/src/Core/JsonSerializer.php +++ b/seed/php-sdk/version-no-default/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/version-no-default/src/Core/SerializableType.php b/seed/php-sdk/version-no-default/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/version-no-default/src/Core/SerializableType.php +++ b/seed/php-sdk/version-no-default/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/version-no-default/src/Core/Union.php b/seed/php-sdk/version-no-default/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/version-no-default/src/Core/Union.php +++ b/seed/php-sdk/version-no-default/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/version-no-default/src/Core/Utils.php b/seed/php-sdk/version-no-default/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/version-no-default/src/Core/Utils.php +++ b/seed/php-sdk/version-no-default/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/version-no-default/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/version-no-default/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/version-no-default/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/version-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/version-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/version-no-default/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/version-no-default/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/version-no-default/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/version-no-default/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/version-no-default/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/version/src/Core/JsonDeserializer.php b/seed/php-sdk/version/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/version/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/version/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/version/src/Core/JsonSerializer.php b/seed/php-sdk/version/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/version/src/Core/JsonSerializer.php +++ b/seed/php-sdk/version/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/version/src/Core/SerializableType.php b/seed/php-sdk/version/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/version/src/Core/SerializableType.php +++ b/seed/php-sdk/version/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/version/src/Core/Union.php b/seed/php-sdk/version/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/version/src/Core/Union.php +++ b/seed/php-sdk/version/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/version/src/Core/Utils.php b/seed/php-sdk/version/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/version/src/Core/Utils.php +++ b/seed/php-sdk/version/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/version/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/version/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/version/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/version/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/version/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/version/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/version/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/version/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/version/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/version/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/version/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/version/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/version/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/version/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/version/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/version/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/version/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/version/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/version/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/version/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/version/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/version/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/version/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/version/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/version/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/version/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/version/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/version/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/version/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/version/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/version/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/version/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/version/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/version/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/version/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/version/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/version/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/version/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/version/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/version/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } } diff --git a/seed/php-sdk/websocket/src/Core/JsonDeserializer.php b/seed/php-sdk/websocket/src/Core/JsonDeserializer.php index 710f3ed2d16..bf11ec0d050 100644 --- a/seed/php-sdk/websocket/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/websocket/src/Core/JsonDeserializer.php @@ -69,11 +69,14 @@ private static function deserializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::deserializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new JsonException("Cannot deserialize value with any of the union types."); + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); } if (is_array($type)) { return self::deserializeArray((array)$data, $type); @@ -112,10 +115,7 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && is_array($data)) { - if (!is_subclass_of($type, SerializableType::class)) { - throw new JsonException("$type is not a subclass of SerializableType."); - } - return $type::jsonDeserialize($data); + return self::deserializeObject($data, $type); } if (gettype($data) === $type) { @@ -125,6 +125,24 @@ private static function deserializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement SerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, SerializableType::class)) { + throw new JsonException("$type is not a subclass of SerializableType."); + } + return $type::jsonDeserialize($data); + } + /** * Deserializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/websocket/src/Core/JsonSerializer.php b/seed/php-sdk/websocket/src/Core/JsonSerializer.php index 9a76b1dc2dc..e6922d75f85 100644 --- a/seed/php-sdk/websocket/src/Core/JsonSerializer.php +++ b/seed/php-sdk/websocket/src/Core/JsonSerializer.php @@ -60,11 +60,11 @@ private static function serializeValue(mixed $data, mixed $type): mixed foreach ($type->types as $unionType) { try { return self::serializeSingleValue($data, $unionType); - } catch (Exception $e) { + } catch (Exception) { continue; } } - throw new \InvalidArgumentException("Cannot serialize value with any of the union types."); + throw new JsonException("Cannot serialize value with any of the union types."); } if (is_array($type)) { @@ -101,10 +101,7 @@ private static function serializeSingleValue(mixed $data, string $type): mixed } if (class_exists($type) && $data instanceof $type) { - if (!is_subclass_of($data, JsonSerializable::class)) { - throw new JsonException("Class $type must implement JsonSerializable."); - } - return $data->jsonSerialize(); + return self::serializeObject($data); } if (gettype($data) === $type) { @@ -114,6 +111,22 @@ private static function serializeSingleValue(mixed $data, string $type): mixed throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); } + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + /** * Serializes a map (associative array) with defined key and value types. * diff --git a/seed/php-sdk/websocket/src/Core/SerializableType.php b/seed/php-sdk/websocket/src/Core/SerializableType.php index c208b26efb0..ecb6c6abc19 100644 --- a/seed/php-sdk/websocket/src/Core/SerializableType.php +++ b/seed/php-sdk/websocket/src/Core/SerializableType.php @@ -4,6 +4,8 @@ use DateTime; use Exception; +use JsonException; +use ReflectionNamedType; use ReflectionProperty; /** @@ -12,7 +14,10 @@ abstract class SerializableType implements \JsonSerializable { /** - * @throws Exception + * Serializes the object to a JSON string. + * + * @return string JSON-encoded string representation of the object. + * @throws Exception If encoding fails. */ public function toJson(): string { @@ -25,8 +30,10 @@ public function toJson(): string } /** - * @return mixed[] - * @throws \JsonException + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. */ public function jsonSerialize(): array { @@ -56,6 +63,11 @@ public function jsonSerialize(): array $value = JsonSerializer::serializeArray($value, $arrayType); } + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + if ($value !== null) { $result[$jsonKey] = $value; } @@ -65,45 +77,42 @@ public function jsonSerialize(): array } /** - * @throws \JsonException - * @throws Exception + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. */ public static function fromJson(string $json): static { $decodedJson = JsonDecoder::decode($json); if (!is_array($decodedJson)) { - throw new \JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); } return self::jsonDeserialize($decodedJson); } /** - * Deserializes an array into an object of the calling class. + * Deserializes an array into an instance of the calling class. * - * @param array $data The array to deserialize. - * @return static - * @throws \JsonException + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. */ public static function jsonDeserialize(array $data): static { $reflectionClass = new \ReflectionClass(static::class); $constructor = $reflectionClass->getConstructor(); - if ($constructor == null) { - throw new \JsonException("No constructor found."); - } - $parameters = $constructor->getParameters(); - $args = []; - foreach ($parameters as $parameter) { - $propertyName = $parameter->getName(); - - if ($reflectionClass->hasProperty($propertyName)) { - $property = $reflectionClass->getProperty($propertyName); - } else { - continue; - } + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + $args = []; + foreach ($reflectionClass->getProperties() as $property) { $jsonKey = self::getJsonKey($property) ?? $property->getName(); + if (array_key_exists($jsonKey, $data)) { $value = $data[$jsonKey]; @@ -112,11 +121,11 @@ public static function jsonDeserialize(array $data): static if ($dateTypeAttr) { $dateType = $dateTypeAttr->newInstance()->type; if (!is_string($value)) { - throw new Exception("Unexpected non-string type for date."); + throw new JsonException("Unexpected non-string type for date."); } $value = ($dateType === DateType::TYPE_DATE) - ? DateTime::createFromFormat(Constant::DateDeserializationFormat, $value) - : new DateTime($value); + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); } // Handle ArrayType annotation @@ -126,20 +135,27 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } - $args[$parameter->getPosition()] = $value; + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; } else { - $args[$parameter->getPosition()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; } } // @phpstan-ignore-next-line - return new static(...$args); + return new static($args); } /** - * Helper function to retrieve the JSON key for a property. + * Retrieves the JSON key associated with a property. * - * @param ReflectionProperty $property - * @return ?string + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. */ private static function getJsonKey(ReflectionProperty $property): ?string { diff --git a/seed/php-sdk/websocket/src/Core/Union.php b/seed/php-sdk/websocket/src/Core/Union.php index 6872f768213..8608d2cae49 100644 --- a/seed/php-sdk/websocket/src/Core/Union.php +++ b/seed/php-sdk/websocket/src/Core/Union.php @@ -13,4 +13,9 @@ public function __construct(string ...$strings) { $this->types = $strings; } + + public function __toString(): string + { + return implode(' | ', $this->types); + } } diff --git a/seed/php-sdk/websocket/src/Core/Utils.php b/seed/php-sdk/websocket/src/Core/Utils.php index c80bbd5969f..74416068d02 100644 --- a/seed/php-sdk/websocket/src/Core/Utils.php +++ b/seed/php-sdk/websocket/src/Core/Utils.php @@ -39,4 +39,23 @@ public static function castKey(mixed $key, string $keyType): mixed default => $key, }; } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } } diff --git a/seed/php-sdk/websocket/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/websocket/tests/Seed/Core/DateArrayTypeTest.php index 3c35c7ab6e1..8d93afc9e44 100644 --- a/seed/php-sdk/websocket/tests/Seed/Core/DateArrayTypeTest.php +++ b/seed/php-sdk/websocket/tests/Seed/Core/DateArrayTypeTest.php @@ -11,14 +11,21 @@ class DateArrayType extends SerializableType { /** - * @param string[] $dates + * @var string[] $dates + */ + #[ArrayType(['date'])] + #[JsonProperty('dates')] + public array $dates; + + /** + * @param array{ + * dates: string[], + * } $values */ public function __construct( - // Array of dates - #[ArrayType(['date'])] - #[JsonProperty('dates')] - public array $dates + array $values, ) { + $this->dates = $values['dates']; } } diff --git a/seed/php-sdk/websocket/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/websocket/tests/Seed/Core/EmptyArraysTest.php index 2bce04d5719..b44f3d093e6 100644 --- a/seed/php-sdk/websocket/tests/Seed/Core/EmptyArraysTest.php +++ b/seed/php-sdk/websocket/tests/Seed/Core/EmptyArraysTest.php @@ -11,21 +11,39 @@ class EmptyArraysType extends SerializableType { /** - * @param string[] $emptyStringArray - * @param array $emptyMapArray - * @param array $emptyDatesArray + * @var string[] $emptyStringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('empty_string_array')] + public array $emptyStringArray; + + /** + * @var array $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values */ public function __construct( - #[ArrayType(['string'])] - #[JsonProperty('empty_string_array')] - public array $emptyStringArray, - #[ArrayType(['integer' => new Union('string', 'null')])] - #[JsonProperty('empty_map_array')] - public array $emptyMapArray, - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('empty_dates_array')] - public array $emptyDatesArray + array $values, ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; } } diff --git a/seed/php-sdk/websocket/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/websocket/tests/Seed/Core/InvalidTypesTest.php index 6e59defbdc8..67bfd235b2f 100644 --- a/seed/php-sdk/websocket/tests/Seed/Core/InvalidTypesTest.php +++ b/seed/php-sdk/websocket/tests/Seed/Core/InvalidTypesTest.php @@ -8,10 +8,21 @@ class InvalidType extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @param array{ + * integerProperty: int, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty + array $values, ) { + $this->integerProperty = $values['integerProperty']; } } diff --git a/seed/php-sdk/websocket/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/websocket/tests/Seed/Core/MixedDateArrayTypeTest.php index a9c9be2ded9..3bf18aec25b 100644 --- a/seed/php-sdk/websocket/tests/Seed/Core/MixedDateArrayTypeTest.php +++ b/seed/php-sdk/websocket/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -11,11 +11,22 @@ class MixedDateArrayType extends SerializableType { + /** + * @var array $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ public function __construct( - #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] - #[JsonProperty('mixed_dates')] - public array $mixedDates + array $values, ) { + $this->mixedDates = $values['mixedDates']; } } diff --git a/seed/php-sdk/websocket/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/websocket/tests/Seed/Core/NestedUnionArrayTypeTest.php index 16d4bc2bb9f..4667ecafcb9 100644 --- a/seed/php-sdk/websocket/tests/Seed/Core/NestedUnionArrayTypeTest.php +++ b/seed/php-sdk/websocket/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -14,23 +14,42 @@ class TestNestedType extends SerializableType { + /** + * @var string $nestedProperty + */ + #[JsonProperty('nested_property')] + public string $nestedProperty; + + /** + * @param array{ + * nestedProperty: string, + * } $values + */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class NestedUnionArrayType extends SerializableType { /** - * @param array> $nestedArray + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values */ public function __construct( - #[ArrayType(['integer' => ['integer' => new Union(TestNestedType::class, 'null', 'date')]])] - #[JsonProperty('nested_array')] - public array $nestedArray + array $values, ) { + $this->nestedArray = $values['nestedArray']; } } diff --git a/seed/php-sdk/websocket/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/websocket/tests/Seed/Core/NullPropertyTypeTest.php index 00b3e1455af..134296f56e3 100644 --- a/seed/php-sdk/websocket/tests/Seed/Core/NullPropertyTypeTest.php +++ b/seed/php-sdk/websocket/tests/Seed/Core/NullPropertyTypeTest.php @@ -8,12 +8,29 @@ class NullPropertyType extends SerializableType { + /** + * @var string $nonNullProperty + */ + #[JsonProperty('non_null_property')] + public string $nonNullProperty; + + /** + * @var string|null $nullProperty + */ + #[JsonProperty('null_property')] + public ?string $nullProperty; + + /** + * @param array{ + * nonNullProperty: string, + * nullProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('non_null_property')] - public string $nonNullProperty, - #[JsonProperty('null_property')] - public ?string $nullProperty = null + array $values, ) { + $this->nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; } } @@ -21,7 +38,7 @@ class NullPropertyTypeTest extends TestCase { public function testNullPropertiesAreOmitted(): void { - $object = new NullPropertyType('Test String', null); + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); $serializedObject = $object->jsonSerialize(); diff --git a/seed/php-sdk/websocket/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/websocket/tests/Seed/Core/NullableArrayTypeTest.php index 4645d003a7c..bf6345e5c6f 100644 --- a/seed/php-sdk/websocket/tests/Seed/Core/NullableArrayTypeTest.php +++ b/seed/php-sdk/websocket/tests/Seed/Core/NullableArrayTypeTest.php @@ -11,13 +11,21 @@ class NullableArrayType extends SerializableType { /** - * @param array $nullableStringArray + * @var array $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values */ public function __construct( - #[ArrayType([new Union('string', 'null')])] - #[JsonProperty('nullable_string_array')] - public array $nullableStringArray + array $values, ) { + $this->nullableStringArray = $values['nullableStringArray']; } } diff --git a/seed/php-sdk/websocket/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/websocket/tests/Seed/Core/ScalarTypesTest.php index 2a91656ed1a..a1da34ff985 100644 --- a/seed/php-sdk/websocket/tests/Seed/Core/ScalarTypesTest.php +++ b/seed/php-sdk/websocket/tests/Seed/Core/ScalarTypesTest.php @@ -10,21 +10,62 @@ class ScalarTypesTest extends SerializableType { + /** + * @var int $integerProperty + */ + #[JsonProperty('integer_property')] + public int $integerProperty; + + /** + * @var float $floatProperty + */ + #[JsonProperty('float_property')] + public float $floatProperty; + + /** + * @var bool $booleanProperty + */ + #[JsonProperty('boolean_property')] + public bool $booleanProperty; + + /** + * @var string $stringProperty + */ + #[JsonProperty('string_property')] + public string $stringProperty; + + /** + * @var array $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ public function __construct( - #[JsonProperty('integer_property')] - public int $integerProperty, - #[JsonProperty('float_property')] - public float $floatProperty, - #[JsonProperty('boolean_property')] - public bool $booleanProperty, - #[JsonProperty('string_property')] - public string $stringProperty, - #[ArrayType([new Union('integer', 'float')])] - #[JsonProperty('int_float_array')] - public array $intFloatArray, - #[JsonProperty('nullable_boolean_property')] - public ?bool $nullableBooleanProperty = null + array $values, ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; } } diff --git a/seed/php-sdk/websocket/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/websocket/tests/Seed/Core/TestTypeTest.php index 30bbe5c9707..8e7ca1b825c 100644 --- a/seed/php-sdk/websocket/tests/Seed/Core/TestTypeTest.php +++ b/seed/php-sdk/websocket/tests/Seed/Core/TestTypeTest.php @@ -13,62 +13,119 @@ class TestNestedType1 extends SerializableType { /** - * @param string $simpleProperty - * @param DateTime $dateProperty - * @param DateTime $datetimeProperty - * @param string[] $stringArray - * @param array $mapProperty - * @param array $objectArray - * @param array> $nestedArray - * @param array $datesArray - * @param string|null $nullableProperty + * @var DateTime $nestedProperty + */ + #[JsonProperty('nested_property')] + #[DateType(DateType::TYPE_DATE)] + public DateTime $nestedProperty; + + /** + * @param array{ + * nestedProperty: DateTime, + * } $values */ public function __construct( - #[JsonProperty('nested_property')] - public string $nestedProperty + array $values, ) { + $this->nestedProperty = $values['nestedProperty']; } } class TestType extends SerializableType { + /** + * @var TestNestedType1 nestedType + */ + #[JsonProperty('nested_type')] + public TestNestedType1 $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[DateType(DateType::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[DateType(DateType::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: TestNestedType1, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ public function __construct( - #[JsonProperty('simple_property')] - public string $simpleProperty, - #[DateType(DateType::TYPE_DATE)] - #[JsonProperty('date_property')] - public DateTime $dateProperty, - #[DateType(DateType::TYPE_DATETIME)] - #[JsonProperty('datetime_property')] - public DateTime $datetimeProperty, - - // Array of strings - #[ArrayType(['string'])] - #[JsonProperty('string_array')] - public array $stringArray, - - // Map with string keys and int values - #[ArrayType(['string' => 'integer'])] - #[JsonProperty('map_property')] - public array $mapProperty, - - // Array of objects or null using Union - #[ArrayType(['integer' => new Union(TestNestedType1::class, 'null')])] - #[JsonProperty('object_array')] - public array $objectArray, - - // Nested array with union types (string|null) - #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] - #[JsonProperty('nested_array')] - public array $nestedArray, - - // Array of dates or null using Union - #[ArrayType([new Union('date', 'null')])] - #[JsonProperty('dates_array')] - public array $datesArray, - #[JsonProperty('nullable_property')] - public ?string $nullableProperty = null // Optional parameter at the end + array $values, ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; } } @@ -81,6 +138,7 @@ public function testSerializationAndDeserialization(): void { // Create test data $data = [ + 'nested_type' => ['nested_property' => '1995-07-20'], 'simple_property' => 'Test String', // 'nullable_property' is omitted to test null serialization 'date_property' => '2023-01-01', @@ -88,7 +146,7 @@ public function testSerializationAndDeserialization(): void 'string_array' => ['one', 'two', 'three'], 'map_property' => ['key1' => 1, 'key2' => 2], 'object_array' => [ - 1 => ['nested_property' => 'Nested One'], + 1 => ['nested_property' => '2021-07-20'], 2 => null, // Testing nullable objects in array ], 'nested_array' => [ @@ -124,7 +182,7 @@ public function testSerializationAndDeserialization(): void // Check object array with nullable elements $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); - $this->assertEquals('Nested One', $object->objectArray[1]->nestedProperty, 'object_array[1]->nestedProperty should match the original data.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); // Check nested array with nullable strings diff --git a/seed/php-sdk/websocket/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/websocket/tests/Seed/Core/UnionArrayTypeTest.php index 250b23c2c9c..8d0998f4b7e 100644 --- a/seed/php-sdk/websocket/tests/Seed/Core/UnionArrayTypeTest.php +++ b/seed/php-sdk/websocket/tests/Seed/Core/UnionArrayTypeTest.php @@ -11,14 +11,21 @@ class UnionArrayType extends SerializableType { /** - * @param array $mixedArray + * @var array $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values */ public function __construct( - // Map with int keys and values that can be string, int, or null using Union - #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] - #[JsonProperty('mixed_array')] - public array $mixedArray + array $values, ) { + $this->mixedArray = $values['mixedArray']; } }