diff --git a/generators/php/codegen/src/AsIs.ts b/generators/php/codegen/src/AsIs.ts index 3a496d31d80..afe44fb2275 100644 --- a/generators/php/codegen/src/AsIs.ts +++ b/generators/php/codegen/src/AsIs.ts @@ -16,6 +16,7 @@ export enum AsIsFiles { NestedUnionArrayTypeTest = "NestedUnionArrayTypeTest.Template.php", NullableArrayTypeTest = "NullableArrayTypeTest.Template.php", NullPropertyTypeTest = "NullPropertyTypeTest.Template.php", + UnionPropertyTypeTest = "UnionPropertyTypeTest.Template.php", ScalarTypesTest = "ScalarTypesTest.Template.php", EnumTest = "EnumTest.Template.php", TestTypeTest = "TestTypeTest.Template.php", diff --git a/generators/php/codegen/src/asIs/JsonDecoder.Template.php b/generators/php/codegen/src/asIs/JsonDecoder.Template.php index cc4238be49a..e34af48812a 100644 --- a/generators/php/codegen/src/asIs/JsonDecoder.Template.php +++ b/generators/php/codegen/src/asIs/JsonDecoder.Template.php @@ -15,7 +15,8 @@ class JsonDecoder * @return string The decoded string. * @throws JsonException If the decoded value is not a string. */ - public static function decodeString(string $json): string { + public static function decodeString(string $json): string + { $decoded = self::decode($json); if (!is_string($decoded)) { throw new JsonException("Unexpected non-string json value: " . $json); @@ -30,7 +31,8 @@ public static function decodeString(string $json): string { * @return bool The decoded boolean. * @throws JsonException If the decoded value is not a boolean. */ - public static function decodeBool(string $json): bool { + public static function decodeBool(string $json): bool + { $decoded = self::decode($json); if (!is_bool($decoded)) { throw new JsonException("Unexpected non-boolean json value: " . $json); @@ -45,7 +47,8 @@ public static function decodeBool(string $json): bool { * @return DateTime The decoded DateTime object. * @throws JsonException If the decoded value is not a valid datetime string. */ - public static function decodeDateTime(string $json): DateTime { + public static function decodeDateTime(string $json): DateTime + { $decoded = self::decode($json); if (!is_string($decoded)) { throw new JsonException("Unexpected non-string json value for datetime: " . $json); @@ -60,7 +63,8 @@ public static function decodeDateTime(string $json): DateTime { * @return DateTime The decoded DateTime object. * @throws JsonException If the decoded value is not a valid date string. */ - public static function decodeDate(string $json): DateTime { + public static function decodeDate(string $json): DateTime + { $decoded = self::decode($json); if (!is_string($decoded)) { throw new JsonException("Unexpected non-string json value for date: " . $json); @@ -75,7 +79,8 @@ public static function decodeDate(string $json): DateTime { * @return float The decoded float. * @throws JsonException If the decoded value is not a float. */ - public static function decodeFloat(string $json): float { + public static function decodeFloat(string $json): float + { $decoded = self::decode($json); if (!is_float($decoded)) { throw new JsonException("Unexpected non-float json value: " . $json); @@ -90,7 +95,8 @@ public static function decodeFloat(string $json): float { * @return int The decoded integer. * @throws JsonException If the decoded value is not an integer. */ - public static function decodeInt(string $json): int { + public static function decodeInt(string $json): int + { $decoded = self::decode($json); if (!is_int($decoded)) { throw new JsonException("Unexpected non-integer json value: " . $json); @@ -106,7 +112,8 @@ public static function decodeInt(string $json): int { * @return mixed[]|array The deserialized array. * @throws JsonException If the decoded value is not an array. */ - public static function decodeArray(string $json, array $type): array { + public static function decodeArray(string $json, array $type): array + { $decoded = self::decode($json); if (!is_array($decoded)) { throw new JsonException("Unexpected non-array json value: " . $json); @@ -114,6 +121,19 @@ public static function decodeArray(string $json, array $type): array { return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * @@ -121,7 +141,8 @@ public static function decodeArray(string $json, array $type): array { * @return mixed The decoded mixed. * @throws JsonException If the decoded value is not an mixed. */ - public static function decodeMixed(string $json): mixed { + public static function decodeMixed(string $json): mixed + { return self::decode($json); } @@ -132,7 +153,8 @@ public static function decodeMixed(string $json): mixed { * @return mixed The decoded value. * @throws JsonException If an error occurs during JSON decoding. */ - public static function decode(string $json): mixed { + public static function decode(string $json): mixed + { return json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR); } -} \ 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 987e8164672..f0a42290111 100644 --- a/generators/php/codegen/src/asIs/JsonDeserializer.Template.php +++ b/generators/php/codegen/src/asIs/JsonDeserializer.Template.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/generators/php/codegen/src/asIs/JsonSerializer.Template.php b/generators/php/codegen/src/asIs/JsonSerializer.Template.php index 27ce301c901..bd046196807 100644 --- a/generators/php/codegen/src/asIs/JsonSerializer.Template.php +++ b/generators/php/codegen/src/asIs/JsonSerializer.Template.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/generators/php/codegen/src/asIs/SerializableType.Template.php b/generators/php/codegen/src/asIs/SerializableType.Template.php index 39acdc2b8ca..6abe274c4c2 100644 --- a/generators/php/codegen/src/asIs/SerializableType.Template.php +++ b/generators/php/codegen/src/asIs/SerializableType.Template.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { @@ -162,4 +176,4 @@ 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/Union.Template.php b/generators/php/codegen/src/asIs/Union.Template.php index 891e1788f98..78aa129b4cc 100644 --- a/generators/php/codegen/src/asIs/Union.Template.php +++ b/generators/php/codegen/src/asIs/Union.Template.php @@ -2,20 +2,61 @@ namespace <%= coreNamespace%>; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } - public function __toString(): string { - return implode(' | ', $this->types); + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ + public function __toString(): string + { + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/generators/php/codegen/src/asIs/UnionPropertyTypeTest.Template.php b/generators/php/codegen/src/asIs/UnionPropertyTypeTest.Template.php new file mode 100644 index 00000000000..4377a6fee9b --- /dev/null +++ b/generators/php/codegen/src/asIs/UnionPropertyTypeTest.Template.php @@ -0,0 +1,118 @@ +; + +use PHPUnit\Framework\TestCase; +use <%= coreNamespace%>\JsonProperty; +use <%= coreNamespace%>\SerializableType; +use <%= coreNamespace%>\Union; + +class UnionPropertyType extends SerializableType +{ + + #[Union(new Union('string', 'integer'), 'null', ['integer' => 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) + { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} \ No newline at end of file diff --git a/generators/php/codegen/src/ast/Attribute.ts b/generators/php/codegen/src/ast/Attribute.ts index 102dc26bd02..5b5d1b45d5a 100644 --- a/generators/php/codegen/src/ast/Attribute.ts +++ b/generators/php/codegen/src/ast/Attribute.ts @@ -26,13 +26,16 @@ export class Attribute extends AstNode { writer.write(`${this.reference.name}`); if (this.arguments.length > 0) { writer.write("("); - for (const argument of this.arguments) { + this.arguments.forEach((argument, index) => { + if (index > 0) { + writer.write(","); + } if (typeof argument === "string") { writer.write(argument); } else { argument.write(writer); } - } + }); writer.write(")"); } } diff --git a/generators/php/codegen/src/ast/Type.ts b/generators/php/codegen/src/ast/Type.ts index 64e8866d992..d2f553c0e46 100644 --- a/generators/php/codegen/src/ast/Type.ts +++ b/generators/php/codegen/src/ast/Type.ts @@ -176,14 +176,17 @@ export class Type extends AstNode { writer.write("}"); break; } - case "union": - this.internalType.types.forEach((type, index) => { + case "union": { + const types = this.getUniqueTypes({ types: this.internalType.types, comment, writer }); + types.forEach((type, index) => { if (index > 0) { writer.write("|"); } - type.write(writer); + type.write(writer, { comment }); + index++; }); break; + } case "optional": { const isUnion = this.internalType.value.internalType.type === "union"; if (!isUnion) { @@ -358,6 +361,34 @@ export class Type extends AstNode { writer.write(": "); entry.valueType.write(writer, { comment }); } + + private getUniqueTypes({ + writer, + types, + comment + }: { + writer: Writer; + types: Type[]; + comment: boolean | undefined; + }): Type[] { + const typeStrings = new Set(); + return types.filter((type) => { + if (comment) { + return true; + } + const typeString = type.toString({ + namespace: writer.namespace, + rootNamespace: writer.rootNamespace, + customConfig: writer.customConfig + }); + // handle potential duplicates, such as strings (due to enums) and arrays + if (typeStrings.has(typeString)) { + return false; + } + typeStrings.add(typeString); + return true; + }); + } } export const DateTimeClassReference = new ClassReference({ diff --git a/generators/php/codegen/src/context/AbstractPhpGeneratorContext.ts b/generators/php/codegen/src/context/AbstractPhpGeneratorContext.ts index b47f7043383..d8678d182d1 100644 --- a/generators/php/codegen/src/context/AbstractPhpGeneratorContext.ts +++ b/generators/php/codegen/src/context/AbstractPhpGeneratorContext.ts @@ -296,7 +296,8 @@ export abstract class AbstractPhpGeneratorContext< AsIsFiles.ScalarTypesTest, AsIsFiles.TestTypeTest, AsIsFiles.UnionArrayTypeTest, - AsIsFiles.EnumTest + AsIsFiles.EnumTest, + AsIsFiles.UnionPropertyTypeTest ]; } diff --git a/generators/php/codegen/src/context/PhpAttributeMapper.ts b/generators/php/codegen/src/context/PhpAttributeMapper.ts index 750840c7b83..32abf78cb48 100644 --- a/generators/php/codegen/src/context/PhpAttributeMapper.ts +++ b/generators/php/codegen/src/context/PhpAttributeMapper.ts @@ -1,7 +1,11 @@ import { assertNever } from "@fern-api/core-utils"; +import { Arguments, UnnamedArgument } from "@fern-api/generator-commons"; import { ObjectProperty } from "@fern-fern/ir-sdk/api"; +import { isEqual, uniq, uniqWith } from "lodash-es"; import { php } from ".."; +import { ClassInstantiation } from "../ast"; import { BasePhpCustomConfigSchema } from "../custom-config/BasePhpCustomConfigSchema"; +import { parameter } from "../php"; import { AbstractPhpGeneratorContext } from "./AbstractPhpGeneratorContext"; export declare namespace PhpAttributeMapper { @@ -41,14 +45,41 @@ export class PhpAttributeMapper { attributes.push( php.attribute({ reference: this.context.getArrayTypeClassReference(), - arguments: [this.getArrayTypeAttributeArgument(type.underlyingType())] + arguments: [this.getTypeAttributeArgument(type.underlyingType())] }) ); } + if (underlyingInternalType.type === "union") { + const unionTypeParameters = this.getUnionTypeParameters(underlyingInternalType.types); + // only add the attribute if deduping in getUnionTypeParameters resulted in more than one type + if (unionTypeParameters.length > 1) { + attributes.push( + php.attribute({ + reference: this.context.getUnionClassReference(), + arguments: this.getUnionTypeParameters(underlyingInternalType.types) + }) + ); + } + } return attributes; } - public getArrayTypeAttributeArgument(type: php.Type): php.AstNode { + public getUnionTypeClassRepresentation(arguments_: php.AstNode[]): ClassInstantiation { + return php.instantiateClass({ + classReference: this.context.getUnionClassReference(), + arguments_ + }); + } + + public getUnionTypeParameters(types: php.Type[]): php.AstNode[] { + // remove duplicates, such as "string" and "string" if enums and strings are both in the union + return uniqWith( + types.map((type) => this.getTypeAttributeArgument(type)), + isEqual + ); + } + + public getTypeAttributeArgument(type: php.Type): php.AstNode { switch (type.internalType.type) { case "int": return php.codeblock("'integer'"); @@ -69,14 +100,14 @@ export class PhpAttributeMapper { return php.codeblock("'object'"); case "array": return php.array({ - entries: [this.getArrayTypeAttributeArgument(type.internalType.value)] + entries: [this.getTypeAttributeArgument(type.internalType.value)] }); case "map": { return php.map({ entries: [ { - key: this.getArrayTypeAttributeArgument(type.internalType.keyType), - value: this.getArrayTypeAttributeArgument(type.internalType.valueType) + key: this.getTypeAttributeArgument(type.internalType.keyType), + value: this.getTypeAttributeArgument(type.internalType.valueType) } ] }); @@ -92,17 +123,19 @@ export class PhpAttributeMapper { }); } case "union": { - return php.instantiateClass({ - classReference: this.context.getUnionClassReference(), - arguments_: type.internalType.types.map((unionType) => - this.getArrayTypeAttributeArgument(unionType) - ) - }); + const unionTypeParameters = this.getUnionTypeParameters(type.internalType.types); + if (unionTypeParameters.length === 1) { + if (unionTypeParameters[0] == null) { + throw new Error("Unexpected empty union type parameters"); + } + return unionTypeParameters[0]; + } + return this.getUnionTypeClassRepresentation(unionTypeParameters); } case "optional": return php.instantiateClass({ classReference: this.context.getUnionClassReference(), - arguments_: [this.getArrayTypeAttributeArgument(type.internalType.value), php.codeblock("'null'")] + arguments_: [this.getTypeAttributeArgument(type.internalType.value), php.codeblock("'null'")] }); case "reference": { const reference = type.internalType.value; diff --git a/generators/php/codegen/src/context/PhpTypeMapper.ts b/generators/php/codegen/src/context/PhpTypeMapper.ts index cddb80ddea5..305d5836886 100644 --- a/generators/php/codegen/src/context/PhpTypeMapper.ts +++ b/generators/php/codegen/src/context/PhpTypeMapper.ts @@ -9,6 +9,7 @@ import { TypeId, TypeReference } from "@fern-fern/ir-sdk/api"; +import { isEqual, uniqWith } from "lodash-es"; import { php } from "../"; import { ClassReference, Type } from "../ast"; import { BasePhpCustomConfigSchema } from "../custom-config/BasePhpCustomConfigSchema"; @@ -118,7 +119,15 @@ export class PhpTypeMapper { case "union": return php.Type.mixed(); case "undiscriminatedUnion": { - return php.Type.mixed(); + return php.Type.union( + // need to dedupe because lists and sets are both represented as array + uniqWith( + typeDeclaration.shape.members.map((member) => + this.convert({ reference: member.type, preserveEnums }) + ), + isEqual + ) + ); } default: assertNever(typeDeclaration.shape); diff --git a/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts b/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts index 065b51fb500..43629e63c96 100644 --- a/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts +++ b/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts @@ -221,6 +221,10 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { methodSuffix: "String" }); case "union": + return this.decodeJsonResponseForUnion({ + arguments_, + types: internalType.types + }); case "object": case "optional": case "typeDict": @@ -259,7 +263,7 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { php.invokeMethod({ on: this.context.getJsonDecoderClassReference(), method: "decodeArray", - arguments_: [...arguments_, this.context.phpAttributeMapper.getArrayTypeAttributeArgument(type)], + arguments_: [...arguments_, this.context.phpAttributeMapper.getTypeAttributeArgument(type)], static_: true }) ); @@ -291,6 +295,34 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { }); } + private decodeJsonResponseForUnion({ + arguments_, + types + }: { + arguments_: UnnamedArgument[]; + types: php.Type[]; + }): php.CodeBlock { + const unionTypeParameters = this.context.phpAttributeMapper.getUnionTypeParameters(types); + // if deduping in getUnionTypeParameters results in one type, treat it like just that type + if (unionTypeParameters.length === 1) { + return this.decodeJsonResponse(types[0]); + } + return php.codeblock((writer) => { + writer.writeNode( + php.invokeMethod({ + on: this.context.getJsonDecoderClassReference(), + method: "decodeUnion", + arguments_: [ + ...arguments_, + this.context.phpAttributeMapper.getUnionTypeClassRepresentation(unionTypeParameters) + ], + static_: true + }) + ); + writer.writeLine("; // @phpstan-ignore-line"); + }); + } + private getResponseBodyContent(): php.CodeBlock { return php.codeblock((writer) => { writer.write(`${JSON_VARIABLE_NAME} = ${RESPONSE_VARIABLE_NAME}->getBody()->getContents()`); diff --git a/generators/php/sdk/src/endpoint/request/EndpointRequest.ts b/generators/php/sdk/src/endpoint/request/EndpointRequest.ts index 39997730a2a..8c6d8a983e3 100644 --- a/generators/php/sdk/src/endpoint/request/EndpointRequest.ts +++ b/generators/php/sdk/src/endpoint/request/EndpointRequest.ts @@ -39,8 +39,23 @@ export abstract class EndpointRequest { protected serializeJsonRequest({ bodyArgument }: { bodyArgument: php.CodeBlock }): php.CodeBlock { const requestParameterType = this.getRequestParameterType(); - const isOptional = requestParameterType.isOptional(); - const underlyingType = this.getRequestParameterType().underlyingType(); + return this.serializeJsonType({ + type: requestParameterType, + bodyArgument, + isOptional: requestParameterType.isOptional() + }); + } + + protected serializeJsonType({ + type, + bodyArgument, + isOptional + }: { + type: php.Type; + bodyArgument: php.CodeBlock; + isOptional: boolean; + }): php.CodeBlock { + const underlyingType = type.underlyingType(); const internalType = underlyingType.internalType; switch (internalType.type) { case "array": @@ -62,6 +77,12 @@ export abstract class EndpointRequest { variant: "DateTime", isOptional }); + case "union": + return this.serializeJsonForUnion({ + bodyArgument, + types: internalType.types, + isOptional + }); case "reference": case "int": case "float": @@ -72,7 +93,6 @@ export abstract class EndpointRequest { case "optional": case "typeDict": case "enumString": - case "union": return bodyArgument; } } @@ -91,7 +111,39 @@ export abstract class EndpointRequest { methodInvocation: php.invokeMethod({ on: this.context.getJsonSerializerClassReference(), method: "serializeArray", - arguments_: [bodyArgument, this.context.phpAttributeMapper.getArrayTypeAttributeArgument(type)], + arguments_: [bodyArgument, this.context.phpAttributeMapper.getTypeAttributeArgument(type)], + static_: true + }), + isOptional + }); + } + + protected serializeJsonForUnion({ + bodyArgument, + types, + isOptional + }: { + bodyArgument: php.CodeBlock; + types: php.Type[]; + isOptional: boolean; + }): php.CodeBlock { + const unionTypeParameters = this.context.phpAttributeMapper.getUnionTypeParameters(types); + // if deduping in getUnionTypeParameters results in one type, treat it like just that type + if (unionTypeParameters.length === 1) { + if (types[0] == null) { + throw new Error("Unexpected empty types"); + } + return this.serializeJsonType({ type: types[0], bodyArgument, isOptional }); + } + return this.serializeJsonRequestMethod({ + bodyArgument, + methodInvocation: php.invokeMethod({ + on: this.context.getJsonSerializerClassReference(), + method: "serializeUnion", + arguments_: [ + bodyArgument, + this.context.phpAttributeMapper.getUnionTypeClassRepresentation(unionTypeParameters) + ], static_: true }), isOptional diff --git a/generators/php/sdk/src/endpoint/request/WrappedEndpointRequest.ts b/generators/php/sdk/src/endpoint/request/WrappedEndpointRequest.ts index e4538231e86..2560506b6e8 100644 --- a/generators/php/sdk/src/endpoint/request/WrappedEndpointRequest.ts +++ b/generators/php/sdk/src/endpoint/request/WrappedEndpointRequest.ts @@ -140,7 +140,16 @@ export class WrappedEndpointRequest extends EndpointRequest { if (maybeLiteral != null) { return php.codeblock(this.context.getLiteralAsString(maybeLiteral)); } - return php.codeblock(`${parameter}`); + const type = this.context.phpTypeMapper.convert({ reference }); + const underlyingInternalType = type.underlyingType().internalType; + if (underlyingInternalType.type === "union") { + return this.serializeJsonForUnion({ + bodyArgument: php.codeblock(parameter), + types: underlyingInternalType.types, + isOptional: false + }); + } + return php.codeblock(parameter); } public getRequestBodyCodeBlock(): RequestBodyCodeBlock | undefined { diff --git a/generators/php/sdk/versions.yml b/generators/php/sdk/versions.yml index 8fd359a75df..c9a63e645a9 100644 --- a/generators/php/sdk/versions.yml +++ b/generators/php/sdk/versions.yml @@ -1,3 +1,8 @@ +- version: 0.1.5 + changelogEntry: + - type: feat + summary: >- + Support undiscriminated unions. - version: 0.1.4 changelogEntry: - type: fix diff --git a/seed/php-model/alias-extends/src/Core/JsonDecoder.php b/seed/php-model/alias-extends/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/alias-extends/src/Core/JsonDecoder.php +++ b/seed/php-model/alias-extends/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/alias-extends/src/Core/JsonDeserializer.php b/seed/php-model/alias-extends/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/alias-extends/src/Core/JsonDeserializer.php +++ b/seed/php-model/alias-extends/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/alias-extends/src/Core/JsonSerializer.php b/seed/php-model/alias-extends/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/alias-extends/src/Core/JsonSerializer.php +++ b/seed/php-model/alias-extends/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/alias-extends/src/Core/SerializableType.php b/seed/php-model/alias-extends/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/alias-extends/src/Core/SerializableType.php +++ b/seed/php-model/alias-extends/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/alias-extends/src/Core/Union.php b/seed/php-model/alias-extends/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/alias-extends/src/Core/Union.php +++ b/seed/php-model/alias-extends/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/alias/src/Core/JsonDecoder.php b/seed/php-model/alias/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/alias/src/Core/JsonDecoder.php +++ b/seed/php-model/alias/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/alias/src/Core/JsonDeserializer.php b/seed/php-model/alias/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/alias/src/Core/JsonDeserializer.php +++ b/seed/php-model/alias/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/alias/src/Core/JsonSerializer.php b/seed/php-model/alias/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/alias/src/Core/JsonSerializer.php +++ b/seed/php-model/alias/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/alias/src/Core/SerializableType.php b/seed/php-model/alias/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/alias/src/Core/SerializableType.php +++ b/seed/php-model/alias/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/alias/src/Core/Union.php b/seed/php-model/alias/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/alias/src/Core/Union.php +++ b/seed/php-model/alias/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/alias/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/alias/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/alias/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/any-auth/src/Core/JsonDecoder.php b/seed/php-model/any-auth/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/any-auth/src/Core/JsonDecoder.php +++ b/seed/php-model/any-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/any-auth/src/Core/JsonDeserializer.php b/seed/php-model/any-auth/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/any-auth/src/Core/JsonDeserializer.php +++ b/seed/php-model/any-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/any-auth/src/Core/JsonSerializer.php b/seed/php-model/any-auth/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/any-auth/src/Core/JsonSerializer.php +++ b/seed/php-model/any-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/any-auth/src/Core/SerializableType.php b/seed/php-model/any-auth/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/any-auth/src/Core/SerializableType.php +++ b/seed/php-model/any-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/any-auth/src/Core/Union.php b/seed/php-model/any-auth/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/any-auth/src/Core/Union.php +++ b/seed/php-model/any-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/api-wide-base-path/src/Core/JsonDecoder.php b/seed/php-model/api-wide-base-path/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/api-wide-base-path/src/Core/JsonDecoder.php +++ b/seed/php-model/api-wide-base-path/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/audiences/src/Core/JsonDecoder.php b/seed/php-model/audiences/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/audiences/src/Core/JsonDecoder.php +++ b/seed/php-model/audiences/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/audiences/src/Core/JsonDeserializer.php b/seed/php-model/audiences/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/audiences/src/Core/JsonDeserializer.php +++ b/seed/php-model/audiences/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/audiences/src/Core/JsonSerializer.php b/seed/php-model/audiences/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/audiences/src/Core/JsonSerializer.php +++ b/seed/php-model/audiences/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/audiences/src/Core/SerializableType.php b/seed/php-model/audiences/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/audiences/src/Core/SerializableType.php +++ b/seed/php-model/audiences/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/audiences/src/Core/Union.php b/seed/php-model/audiences/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/audiences/src/Core/Union.php +++ b/seed/php-model/audiences/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/audiences/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/audiences/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/audiences/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/auth-environment-variables/src/Core/JsonDecoder.php b/seed/php-model/auth-environment-variables/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/auth-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-model/auth-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/auth-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-model/auth-environment-variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-model/auth-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-model/auth-environment-variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/auth-environment-variables/src/Core/SerializableType.php +++ b/seed/php-model/auth-environment-variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/auth-environment-variables/src/Core/Union.php +++ b/seed/php-model/auth-environment-variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/basic-auth-environment-variables/src/Core/JsonDecoder.php b/seed/php-model/basic-auth-environment-variables/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/basic-auth-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-model/basic-auth-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/basic-auth/src/Core/JsonDecoder.php b/seed/php-model/basic-auth/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/basic-auth/src/Core/JsonDecoder.php +++ b/seed/php-model/basic-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/basic-auth/src/Core/JsonDeserializer.php b/seed/php-model/basic-auth/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/basic-auth/src/Core/JsonDeserializer.php +++ b/seed/php-model/basic-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/basic-auth/src/Core/JsonSerializer.php b/seed/php-model/basic-auth/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/basic-auth/src/Core/JsonSerializer.php +++ b/seed/php-model/basic-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/basic-auth/src/Core/SerializableType.php b/seed/php-model/basic-auth/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/basic-auth/src/Core/SerializableType.php +++ b/seed/php-model/basic-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/basic-auth/src/Core/Union.php b/seed/php-model/basic-auth/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/basic-auth/src/Core/Union.php +++ b/seed/php-model/basic-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/bearer-token-environment-variable/src/Core/JsonDecoder.php b/seed/php-model/bearer-token-environment-variable/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/bearer-token-environment-variable/src/Core/JsonDecoder.php +++ b/seed/php-model/bearer-token-environment-variable/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/bytes/src/Core/JsonDecoder.php b/seed/php-model/bytes/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/bytes/src/Core/JsonDecoder.php +++ b/seed/php-model/bytes/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/bytes/src/Core/JsonDeserializer.php b/seed/php-model/bytes/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/bytes/src/Core/JsonDeserializer.php +++ b/seed/php-model/bytes/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/bytes/src/Core/JsonSerializer.php b/seed/php-model/bytes/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/bytes/src/Core/JsonSerializer.php +++ b/seed/php-model/bytes/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/bytes/src/Core/SerializableType.php b/seed/php-model/bytes/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/bytes/src/Core/SerializableType.php +++ b/seed/php-model/bytes/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/bytes/src/Core/Union.php b/seed/php-model/bytes/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/bytes/src/Core/Union.php +++ b/seed/php-model/bytes/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/bytes/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/bytes/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/bytes/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/circular-references-advanced/src/Core/JsonDecoder.php b/seed/php-model/circular-references-advanced/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/circular-references-advanced/src/Core/JsonDecoder.php +++ b/seed/php-model/circular-references-advanced/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/circular-references-advanced/src/Core/JsonDeserializer.php +++ b/seed/php-model/circular-references-advanced/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-model/circular-references-advanced/src/Core/JsonSerializer.php +++ b/seed/php-model/circular-references-advanced/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/circular-references-advanced/src/Core/SerializableType.php +++ b/seed/php-model/circular-references-advanced/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/circular-references-advanced/src/Core/Union.php +++ b/seed/php-model/circular-references-advanced/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/circular-references/src/Core/JsonDecoder.php b/seed/php-model/circular-references/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/circular-references/src/Core/JsonDecoder.php +++ b/seed/php-model/circular-references/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/circular-references/src/Core/JsonDeserializer.php b/seed/php-model/circular-references/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/circular-references/src/Core/JsonDeserializer.php +++ b/seed/php-model/circular-references/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/circular-references/src/Core/JsonSerializer.php b/seed/php-model/circular-references/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/circular-references/src/Core/JsonSerializer.php +++ b/seed/php-model/circular-references/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/circular-references/src/Core/SerializableType.php b/seed/php-model/circular-references/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/circular-references/src/Core/SerializableType.php +++ b/seed/php-model/circular-references/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/circular-references/src/Core/Union.php b/seed/php-model/circular-references/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/circular-references/src/Core/Union.php +++ b/seed/php-model/circular-references/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/cross-package-type-names/src/Core/JsonDecoder.php b/seed/php-model/cross-package-type-names/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/cross-package-type-names/src/Core/JsonDecoder.php +++ b/seed/php-model/cross-package-type-names/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/custom-auth/src/Core/JsonDecoder.php b/seed/php-model/custom-auth/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/custom-auth/src/Core/JsonDecoder.php +++ b/seed/php-model/custom-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/custom-auth/src/Core/JsonDeserializer.php b/seed/php-model/custom-auth/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/custom-auth/src/Core/JsonDeserializer.php +++ b/seed/php-model/custom-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/custom-auth/src/Core/JsonSerializer.php b/seed/php-model/custom-auth/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/custom-auth/src/Core/JsonSerializer.php +++ b/seed/php-model/custom-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/custom-auth/src/Core/SerializableType.php b/seed/php-model/custom-auth/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/custom-auth/src/Core/SerializableType.php +++ b/seed/php-model/custom-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/custom-auth/src/Core/Union.php b/seed/php-model/custom-auth/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/custom-auth/src/Core/Union.php +++ b/seed/php-model/custom-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/enum/src/Core/JsonDecoder.php b/seed/php-model/enum/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/enum/src/Core/JsonDecoder.php +++ b/seed/php-model/enum/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/enum/src/Core/JsonDeserializer.php b/seed/php-model/enum/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/enum/src/Core/JsonDeserializer.php +++ b/seed/php-model/enum/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/enum/src/Core/JsonSerializer.php b/seed/php-model/enum/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/enum/src/Core/JsonSerializer.php +++ b/seed/php-model/enum/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/enum/src/Core/SerializableType.php b/seed/php-model/enum/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/enum/src/Core/SerializableType.php +++ b/seed/php-model/enum/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/enum/src/Core/Union.php b/seed/php-model/enum/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/enum/src/Core/Union.php +++ b/seed/php-model/enum/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/enum/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/enum/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/enum/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/error-property/src/Core/JsonDecoder.php b/seed/php-model/error-property/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/error-property/src/Core/JsonDecoder.php +++ b/seed/php-model/error-property/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/error-property/src/Core/JsonDeserializer.php b/seed/php-model/error-property/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/error-property/src/Core/JsonDeserializer.php +++ b/seed/php-model/error-property/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/error-property/src/Core/JsonSerializer.php b/seed/php-model/error-property/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/error-property/src/Core/JsonSerializer.php +++ b/seed/php-model/error-property/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/error-property/src/Core/SerializableType.php b/seed/php-model/error-property/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/error-property/src/Core/SerializableType.php +++ b/seed/php-model/error-property/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/error-property/src/Core/Union.php b/seed/php-model/error-property/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/error-property/src/Core/Union.php +++ b/seed/php-model/error-property/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/error-property/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/error-property/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/error-property/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/examples/src/Core/JsonDecoder.php b/seed/php-model/examples/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/examples/src/Core/JsonDecoder.php +++ b/seed/php-model/examples/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/examples/src/Core/JsonDeserializer.php b/seed/php-model/examples/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/examples/src/Core/JsonDeserializer.php +++ b/seed/php-model/examples/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/examples/src/Core/JsonSerializer.php b/seed/php-model/examples/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/examples/src/Core/JsonSerializer.php +++ b/seed/php-model/examples/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/examples/src/Core/SerializableType.php b/seed/php-model/examples/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/examples/src/Core/SerializableType.php +++ b/seed/php-model/examples/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/examples/src/Core/Union.php b/seed/php-model/examples/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/examples/src/Core/Union.php +++ b/seed/php-model/examples/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/examples/src/Identifier.php b/seed/php-model/examples/src/Identifier.php index e673d13197e..48363b1db26 100644 --- a/seed/php-model/examples/src/Identifier.php +++ b/seed/php-model/examples/src/Identifier.php @@ -8,10 +8,10 @@ class Identifier extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @var string $value @@ -27,7 +27,7 @@ class Identifier extends SerializableType /** * @param array{ - * type: mixed, + * type: value-of|value-of, * value: string, * label: string, * } $values diff --git a/seed/php-model/examples/src/Types/Entity.php b/seed/php-model/examples/src/Types/Entity.php index c1dd816bc67..c94f68a9794 100644 --- a/seed/php-model/examples/src/Types/Entity.php +++ b/seed/php-model/examples/src/Types/Entity.php @@ -3,15 +3,17 @@ namespace Seed\Types; use Seed\Core\SerializableType; +use Seed\BasicType; +use Seed\ComplexType; use Seed\Core\JsonProperty; class Entity extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @var string $name @@ -21,7 +23,7 @@ class Entity extends SerializableType /** * @param array{ - * type: mixed, + * type: value-of|value-of, * name: string, * } $values */ diff --git a/seed/php-model/examples/src/Types/ResponseType.php b/seed/php-model/examples/src/Types/ResponseType.php index fd26020ff99..6bdca9a0b71 100644 --- a/seed/php-model/examples/src/Types/ResponseType.php +++ b/seed/php-model/examples/src/Types/ResponseType.php @@ -3,19 +3,21 @@ namespace Seed\Types; use Seed\Core\SerializableType; +use Seed\BasicType; +use Seed\ComplexType; use Seed\Core\JsonProperty; class ResponseType extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @param array{ - * type: mixed, + * type: value-of|value-of, * } $values */ public function __construct( diff --git a/seed/php-model/examples/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/examples/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/examples/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/exhaustive/src/Core/JsonDecoder.php b/seed/php-model/exhaustive/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/exhaustive/src/Core/JsonDecoder.php +++ b/seed/php-model/exhaustive/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/exhaustive/src/Core/JsonDeserializer.php b/seed/php-model/exhaustive/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-model/exhaustive/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/exhaustive/src/Core/JsonSerializer.php b/seed/php-model/exhaustive/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-model/exhaustive/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/exhaustive/src/Core/SerializableType.php b/seed/php-model/exhaustive/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/exhaustive/src/Core/SerializableType.php +++ b/seed/php-model/exhaustive/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/exhaustive/src/Core/Union.php b/seed/php-model/exhaustive/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/exhaustive/src/Core/Union.php +++ b/seed/php-model/exhaustive/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/extends/src/Core/JsonDecoder.php b/seed/php-model/extends/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/extends/src/Core/JsonDecoder.php +++ b/seed/php-model/extends/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/extends/src/Core/JsonDeserializer.php b/seed/php-model/extends/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/extends/src/Core/JsonDeserializer.php +++ b/seed/php-model/extends/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/extends/src/Core/JsonSerializer.php b/seed/php-model/extends/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/extends/src/Core/JsonSerializer.php +++ b/seed/php-model/extends/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/extends/src/Core/SerializableType.php b/seed/php-model/extends/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/extends/src/Core/SerializableType.php +++ b/seed/php-model/extends/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/extends/src/Core/Union.php b/seed/php-model/extends/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/extends/src/Core/Union.php +++ b/seed/php-model/extends/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/extends/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/extends/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/extends/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/extra-properties/src/Core/JsonDecoder.php b/seed/php-model/extra-properties/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/extra-properties/src/Core/JsonDecoder.php +++ b/seed/php-model/extra-properties/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/extra-properties/src/Core/JsonDeserializer.php b/seed/php-model/extra-properties/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/extra-properties/src/Core/JsonDeserializer.php +++ b/seed/php-model/extra-properties/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/extra-properties/src/Core/JsonSerializer.php b/seed/php-model/extra-properties/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/extra-properties/src/Core/JsonSerializer.php +++ b/seed/php-model/extra-properties/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/extra-properties/src/Core/SerializableType.php b/seed/php-model/extra-properties/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/extra-properties/src/Core/SerializableType.php +++ b/seed/php-model/extra-properties/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/extra-properties/src/Core/Union.php b/seed/php-model/extra-properties/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/extra-properties/src/Core/Union.php +++ b/seed/php-model/extra-properties/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/file-download/src/Core/JsonDecoder.php b/seed/php-model/file-download/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/file-download/src/Core/JsonDecoder.php +++ b/seed/php-model/file-download/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/file-download/src/Core/JsonDeserializer.php b/seed/php-model/file-download/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/file-download/src/Core/JsonDeserializer.php +++ b/seed/php-model/file-download/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/file-download/src/Core/JsonSerializer.php b/seed/php-model/file-download/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/file-download/src/Core/JsonSerializer.php +++ b/seed/php-model/file-download/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/file-download/src/Core/SerializableType.php b/seed/php-model/file-download/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/file-download/src/Core/SerializableType.php +++ b/seed/php-model/file-download/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/file-download/src/Core/Union.php b/seed/php-model/file-download/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/file-download/src/Core/Union.php +++ b/seed/php-model/file-download/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/file-download/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/file-download/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/file-download/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/file-upload/src/Core/JsonDecoder.php b/seed/php-model/file-upload/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/file-upload/src/Core/JsonDecoder.php +++ b/seed/php-model/file-upload/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/file-upload/src/Core/JsonDeserializer.php b/seed/php-model/file-upload/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/file-upload/src/Core/JsonDeserializer.php +++ b/seed/php-model/file-upload/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/file-upload/src/Core/JsonSerializer.php b/seed/php-model/file-upload/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/file-upload/src/Core/JsonSerializer.php +++ b/seed/php-model/file-upload/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/file-upload/src/Core/SerializableType.php b/seed/php-model/file-upload/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/file-upload/src/Core/SerializableType.php +++ b/seed/php-model/file-upload/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/file-upload/src/Core/Union.php b/seed/php-model/file-upload/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/file-upload/src/Core/Union.php +++ b/seed/php-model/file-upload/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/folders/src/Core/JsonDecoder.php b/seed/php-model/folders/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/folders/src/Core/JsonDecoder.php +++ b/seed/php-model/folders/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/folders/src/Core/JsonDeserializer.php b/seed/php-model/folders/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/folders/src/Core/JsonDeserializer.php +++ b/seed/php-model/folders/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/folders/src/Core/JsonSerializer.php b/seed/php-model/folders/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/folders/src/Core/JsonSerializer.php +++ b/seed/php-model/folders/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/folders/src/Core/SerializableType.php b/seed/php-model/folders/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/folders/src/Core/SerializableType.php +++ b/seed/php-model/folders/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/folders/src/Core/Union.php b/seed/php-model/folders/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/folders/src/Core/Union.php +++ b/seed/php-model/folders/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/folders/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/folders/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/folders/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/grpc-proto-exhaustive/src/Column.php b/seed/php-model/grpc-proto-exhaustive/src/Column.php index 0d6262dcadf..c0cc5b4386c 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Column.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Column.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class Column extends SerializableType { @@ -21,10 +22,10 @@ class Column extends SerializableType public array $values; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @var ?IndexedData $indexedData @@ -36,7 +37,7 @@ class Column extends SerializableType * @param array{ * id: string, * values: array, - * metadata: mixed, + * metadata?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -45,7 +46,7 @@ public function __construct( ) { $this->id = $values['id']; $this->values = $values['values']; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDecoder.php b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDecoder.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/SerializableType.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/Union.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php b/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php index a21ff3222ff..c30b57e5a54 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php +++ b/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class QueryColumn extends SerializableType { @@ -27,10 +28,10 @@ class QueryColumn extends SerializableType public ?string $namespace; /** - * @var mixed $filter + * @var array|array|null $filter */ - #[JsonProperty('filter')] - public mixed $filter; + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $filter; /** * @var ?IndexedData $indexedData @@ -43,7 +44,7 @@ class QueryColumn extends SerializableType * values: array, * topK?: ?int, * namespace?: ?string, - * filter: mixed, + * filter?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -53,7 +54,7 @@ public function __construct( $this->values = $values['values']; $this->topK = $values['topK'] ?? null; $this->namespace = $values['namespace'] ?? null; - $this->filter = $values['filter']; + $this->filter = $values['filter'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php b/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php index 021ff382e18..22ce1d2861d 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php +++ b/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class ScoredColumn extends SerializableType { @@ -27,10 +28,10 @@ class ScoredColumn extends SerializableType public ?array $values; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @var ?IndexedData $indexedData @@ -43,7 +44,7 @@ class ScoredColumn extends SerializableType * id: string, * score?: ?float, * values?: ?array, - * metadata: mixed, + * metadata?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -53,7 +54,7 @@ public function __construct( $this->id = $values['id']; $this->score = $values['score'] ?? null; $this->values = $values['values'] ?? null; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/grpc-proto/src/Core/JsonDecoder.php b/seed/php-model/grpc-proto/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/grpc-proto/src/Core/JsonDecoder.php +++ b/seed/php-model/grpc-proto/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php b/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php +++ b/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/grpc-proto/src/Core/JsonSerializer.php b/seed/php-model/grpc-proto/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/grpc-proto/src/Core/JsonSerializer.php +++ b/seed/php-model/grpc-proto/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/grpc-proto/src/Core/SerializableType.php b/seed/php-model/grpc-proto/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/grpc-proto/src/Core/SerializableType.php +++ b/seed/php-model/grpc-proto/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/grpc-proto/src/Core/Union.php b/seed/php-model/grpc-proto/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/grpc-proto/src/Core/Union.php +++ b/seed/php-model/grpc-proto/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/grpc-proto/src/UserModel.php b/seed/php-model/grpc-proto/src/UserModel.php index 4ba096631ea..54516ab941a 100644 --- a/seed/php-model/grpc-proto/src/UserModel.php +++ b/seed/php-model/grpc-proto/src/UserModel.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; +use Seed\Core\Union; class UserModel extends SerializableType { @@ -32,10 +33,10 @@ class UserModel extends SerializableType public ?float $weight; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @param array{ @@ -43,16 +44,16 @@ class UserModel extends SerializableType * email?: ?string, * age?: ?int, * weight?: ?float, - * metadata: mixed, + * metadata?: array|array|null, * } $values */ public function __construct( - array $values, + array $values = [], ) { $this->username = $values['username'] ?? null; $this->email = $values['email'] ?? null; $this->age = $values['age'] ?? null; $this->weight = $values['weight'] ?? null; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; } } diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/idempotency-headers/src/Core/JsonDecoder.php b/seed/php-model/idempotency-headers/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/idempotency-headers/src/Core/JsonDecoder.php +++ b/seed/php-model/idempotency-headers/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php b/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php +++ b/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php b/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php +++ b/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/idempotency-headers/src/Core/SerializableType.php b/seed/php-model/idempotency-headers/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/idempotency-headers/src/Core/SerializableType.php +++ b/seed/php-model/idempotency-headers/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/idempotency-headers/src/Core/Union.php b/seed/php-model/idempotency-headers/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/idempotency-headers/src/Core/Union.php +++ b/seed/php-model/idempotency-headers/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/imdb/src/Core/JsonDecoder.php b/seed/php-model/imdb/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/imdb/src/Core/JsonDecoder.php +++ b/seed/php-model/imdb/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/imdb/src/Core/JsonDeserializer.php b/seed/php-model/imdb/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/imdb/src/Core/JsonDeserializer.php +++ b/seed/php-model/imdb/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/imdb/src/Core/JsonSerializer.php b/seed/php-model/imdb/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/imdb/src/Core/JsonSerializer.php +++ b/seed/php-model/imdb/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/imdb/src/Core/SerializableType.php b/seed/php-model/imdb/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/imdb/src/Core/SerializableType.php +++ b/seed/php-model/imdb/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/imdb/src/Core/Union.php b/seed/php-model/imdb/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/imdb/src/Core/Union.php +++ b/seed/php-model/imdb/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/imdb/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/imdb/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/imdb/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/literal/src/Core/JsonDecoder.php b/seed/php-model/literal/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/literal/src/Core/JsonDecoder.php +++ b/seed/php-model/literal/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/literal/src/Core/JsonDeserializer.php b/seed/php-model/literal/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/literal/src/Core/JsonDeserializer.php +++ b/seed/php-model/literal/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/literal/src/Core/JsonSerializer.php b/seed/php-model/literal/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/literal/src/Core/JsonSerializer.php +++ b/seed/php-model/literal/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/literal/src/Core/SerializableType.php b/seed/php-model/literal/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/literal/src/Core/SerializableType.php +++ b/seed/php-model/literal/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/literal/src/Core/Union.php b/seed/php-model/literal/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/literal/src/Core/Union.php +++ b/seed/php-model/literal/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/literal/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/literal/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/literal/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/mixed-case/src/Core/JsonDecoder.php b/seed/php-model/mixed-case/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/mixed-case/src/Core/JsonDecoder.php +++ b/seed/php-model/mixed-case/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/mixed-case/src/Core/JsonDeserializer.php b/seed/php-model/mixed-case/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/mixed-case/src/Core/JsonDeserializer.php +++ b/seed/php-model/mixed-case/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/mixed-case/src/Core/JsonSerializer.php b/seed/php-model/mixed-case/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/mixed-case/src/Core/JsonSerializer.php +++ b/seed/php-model/mixed-case/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/mixed-case/src/Core/SerializableType.php b/seed/php-model/mixed-case/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/mixed-case/src/Core/SerializableType.php +++ b/seed/php-model/mixed-case/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/mixed-case/src/Core/Union.php b/seed/php-model/mixed-case/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/mixed-case/src/Core/Union.php +++ b/seed/php-model/mixed-case/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/mixed-file-directory/src/Core/JsonDecoder.php b/seed/php-model/mixed-file-directory/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/mixed-file-directory/src/Core/JsonDecoder.php +++ b/seed/php-model/mixed-file-directory/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/mixed-file-directory/src/Core/JsonDeserializer.php +++ b/seed/php-model/mixed-file-directory/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-model/mixed-file-directory/src/Core/JsonSerializer.php +++ b/seed/php-model/mixed-file-directory/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/mixed-file-directory/src/Core/SerializableType.php +++ b/seed/php-model/mixed-file-directory/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/mixed-file-directory/src/Core/Union.php +++ b/seed/php-model/mixed-file-directory/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/multi-line-docs/src/Core/JsonDecoder.php b/seed/php-model/multi-line-docs/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/multi-line-docs/src/Core/JsonDecoder.php +++ b/seed/php-model/multi-line-docs/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/multi-line-docs/src/Core/JsonDeserializer.php +++ b/seed/php-model/multi-line-docs/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-model/multi-line-docs/src/Core/JsonSerializer.php +++ b/seed/php-model/multi-line-docs/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/multi-line-docs/src/Core/SerializableType.php +++ b/seed/php-model/multi-line-docs/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/multi-line-docs/src/Core/Union.php +++ b/seed/php-model/multi-line-docs/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/multi-url-environment-no-default/src/Core/JsonDecoder.php b/seed/php-model/multi-url-environment-no-default/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/multi-url-environment-no-default/src/Core/JsonDecoder.php +++ b/seed/php-model/multi-url-environment-no-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/multi-url-environment/src/Core/JsonDecoder.php b/seed/php-model/multi-url-environment/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/multi-url-environment/src/Core/JsonDecoder.php +++ b/seed/php-model/multi-url-environment/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/multi-url-environment/src/Core/JsonDeserializer.php +++ b/seed/php-model/multi-url-environment/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-model/multi-url-environment/src/Core/JsonSerializer.php +++ b/seed/php-model/multi-url-environment/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/multi-url-environment/src/Core/SerializableType.php +++ b/seed/php-model/multi-url-environment/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/multi-url-environment/src/Core/Union.php +++ b/seed/php-model/multi-url-environment/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/no-environment/src/Core/JsonDecoder.php b/seed/php-model/no-environment/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/no-environment/src/Core/JsonDecoder.php +++ b/seed/php-model/no-environment/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/no-environment/src/Core/JsonDeserializer.php b/seed/php-model/no-environment/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/no-environment/src/Core/JsonDeserializer.php +++ b/seed/php-model/no-environment/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/no-environment/src/Core/JsonSerializer.php b/seed/php-model/no-environment/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/no-environment/src/Core/JsonSerializer.php +++ b/seed/php-model/no-environment/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/no-environment/src/Core/SerializableType.php b/seed/php-model/no-environment/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/no-environment/src/Core/SerializableType.php +++ b/seed/php-model/no-environment/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/no-environment/src/Core/Union.php b/seed/php-model/no-environment/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/no-environment/src/Core/Union.php +++ b/seed/php-model/no-environment/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/oauth-client-credentials-default/src/Core/JsonDecoder.php b/seed/php-model/oauth-client-credentials-default/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/oauth-client-credentials-default/src/Core/JsonDecoder.php +++ b/seed/php-model/oauth-client-credentials-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php +++ b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/oauth-client-credentials/src/Core/JsonDecoder.php b/seed/php-model/oauth-client-credentials/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/JsonDecoder.php +++ b/seed/php-model/oauth-client-credentials/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/JsonDeserializer.php +++ b/seed/php-model/oauth-client-credentials/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/JsonSerializer.php +++ b/seed/php-model/oauth-client-credentials/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/SerializableType.php +++ b/seed/php-model/oauth-client-credentials/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/Union.php +++ b/seed/php-model/oauth-client-credentials/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/object/src/Core/JsonDecoder.php b/seed/php-model/object/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/object/src/Core/JsonDecoder.php +++ b/seed/php-model/object/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/object/src/Core/JsonDeserializer.php b/seed/php-model/object/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/object/src/Core/JsonDeserializer.php +++ b/seed/php-model/object/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/object/src/Core/JsonSerializer.php b/seed/php-model/object/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/object/src/Core/JsonSerializer.php +++ b/seed/php-model/object/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/object/src/Core/SerializableType.php b/seed/php-model/object/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/object/src/Core/SerializableType.php +++ b/seed/php-model/object/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/object/src/Core/Union.php b/seed/php-model/object/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/object/src/Core/Union.php +++ b/seed/php-model/object/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/object/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/object/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/object/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/objects-with-imports/src/Core/JsonDecoder.php b/seed/php-model/objects-with-imports/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/objects-with-imports/src/Core/JsonDecoder.php +++ b/seed/php-model/objects-with-imports/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/objects-with-imports/src/Core/JsonDeserializer.php +++ b/seed/php-model/objects-with-imports/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-model/objects-with-imports/src/Core/JsonSerializer.php +++ b/seed/php-model/objects-with-imports/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/objects-with-imports/src/Core/SerializableType.php +++ b/seed/php-model/objects-with-imports/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/objects-with-imports/src/Core/Union.php +++ b/seed/php-model/objects-with-imports/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/optional/src/Core/JsonDecoder.php b/seed/php-model/optional/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/optional/src/Core/JsonDecoder.php +++ b/seed/php-model/optional/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/optional/src/Core/JsonDeserializer.php b/seed/php-model/optional/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/optional/src/Core/JsonDeserializer.php +++ b/seed/php-model/optional/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/optional/src/Core/JsonSerializer.php b/seed/php-model/optional/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/optional/src/Core/JsonSerializer.php +++ b/seed/php-model/optional/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/optional/src/Core/SerializableType.php b/seed/php-model/optional/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/optional/src/Core/SerializableType.php +++ b/seed/php-model/optional/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/optional/src/Core/Union.php b/seed/php-model/optional/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/optional/src/Core/Union.php +++ b/seed/php-model/optional/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/optional/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/optional/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/optional/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/package-yml/src/Core/JsonDecoder.php b/seed/php-model/package-yml/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/package-yml/src/Core/JsonDecoder.php +++ b/seed/php-model/package-yml/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/package-yml/src/Core/JsonDeserializer.php b/seed/php-model/package-yml/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/package-yml/src/Core/JsonDeserializer.php +++ b/seed/php-model/package-yml/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/package-yml/src/Core/JsonSerializer.php b/seed/php-model/package-yml/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/package-yml/src/Core/JsonSerializer.php +++ b/seed/php-model/package-yml/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/package-yml/src/Core/SerializableType.php b/seed/php-model/package-yml/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/package-yml/src/Core/SerializableType.php +++ b/seed/php-model/package-yml/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/package-yml/src/Core/Union.php b/seed/php-model/package-yml/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/package-yml/src/Core/Union.php +++ b/seed/php-model/package-yml/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/pagination/src/Core/JsonDecoder.php b/seed/php-model/pagination/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/pagination/src/Core/JsonDecoder.php +++ b/seed/php-model/pagination/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/pagination/src/Core/JsonDeserializer.php b/seed/php-model/pagination/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/pagination/src/Core/JsonDeserializer.php +++ b/seed/php-model/pagination/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/pagination/src/Core/JsonSerializer.php b/seed/php-model/pagination/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/pagination/src/Core/JsonSerializer.php +++ b/seed/php-model/pagination/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/pagination/src/Core/SerializableType.php b/seed/php-model/pagination/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/pagination/src/Core/SerializableType.php +++ b/seed/php-model/pagination/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/pagination/src/Core/Union.php b/seed/php-model/pagination/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/pagination/src/Core/Union.php +++ b/seed/php-model/pagination/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/pagination/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/pagination/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/pagination/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/plain-text/src/Core/JsonDecoder.php b/seed/php-model/plain-text/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/plain-text/src/Core/JsonDecoder.php +++ b/seed/php-model/plain-text/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/plain-text/src/Core/JsonDeserializer.php b/seed/php-model/plain-text/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/plain-text/src/Core/JsonDeserializer.php +++ b/seed/php-model/plain-text/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/plain-text/src/Core/JsonSerializer.php b/seed/php-model/plain-text/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/plain-text/src/Core/JsonSerializer.php +++ b/seed/php-model/plain-text/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/plain-text/src/Core/SerializableType.php b/seed/php-model/plain-text/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/plain-text/src/Core/SerializableType.php +++ b/seed/php-model/plain-text/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/plain-text/src/Core/Union.php b/seed/php-model/plain-text/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/plain-text/src/Core/Union.php +++ b/seed/php-model/plain-text/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/query-parameters/src/Core/JsonDecoder.php b/seed/php-model/query-parameters/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/query-parameters/src/Core/JsonDecoder.php +++ b/seed/php-model/query-parameters/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/query-parameters/src/Core/JsonDeserializer.php b/seed/php-model/query-parameters/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/query-parameters/src/Core/JsonDeserializer.php +++ b/seed/php-model/query-parameters/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/query-parameters/src/Core/JsonSerializer.php b/seed/php-model/query-parameters/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/query-parameters/src/Core/JsonSerializer.php +++ b/seed/php-model/query-parameters/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/query-parameters/src/Core/SerializableType.php b/seed/php-model/query-parameters/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/query-parameters/src/Core/SerializableType.php +++ b/seed/php-model/query-parameters/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/query-parameters/src/Core/Union.php b/seed/php-model/query-parameters/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/query-parameters/src/Core/Union.php +++ b/seed/php-model/query-parameters/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/reserved-keywords/src/Core/JsonDecoder.php b/seed/php-model/reserved-keywords/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/reserved-keywords/src/Core/JsonDecoder.php +++ b/seed/php-model/reserved-keywords/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php b/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php +++ b/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php b/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php +++ b/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/reserved-keywords/src/Core/SerializableType.php b/seed/php-model/reserved-keywords/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/reserved-keywords/src/Core/SerializableType.php +++ b/seed/php-model/reserved-keywords/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/reserved-keywords/src/Core/Union.php b/seed/php-model/reserved-keywords/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/reserved-keywords/src/Core/Union.php +++ b/seed/php-model/reserved-keywords/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/response-property/src/Core/JsonDecoder.php b/seed/php-model/response-property/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/response-property/src/Core/JsonDecoder.php +++ b/seed/php-model/response-property/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/response-property/src/Core/JsonDeserializer.php b/seed/php-model/response-property/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/response-property/src/Core/JsonDeserializer.php +++ b/seed/php-model/response-property/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/response-property/src/Core/JsonSerializer.php b/seed/php-model/response-property/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/response-property/src/Core/JsonSerializer.php +++ b/seed/php-model/response-property/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/response-property/src/Core/SerializableType.php b/seed/php-model/response-property/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/response-property/src/Core/SerializableType.php +++ b/seed/php-model/response-property/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/response-property/src/Core/Union.php b/seed/php-model/response-property/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/response-property/src/Core/Union.php +++ b/seed/php-model/response-property/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/response-property/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/response-property/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/response-property/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/server-sent-event-examples/.github/workflows/ci.yml b/seed/php-model/server-sent-event-examples/.github/workflows/ci.yml new file mode 100644 index 00000000000..258bf33a19f --- /dev/null +++ b/seed/php-model/server-sent-event-examples/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + + - name: Install tools + run: | + composer install + + - name: Build + run: | + composer build + + - name: Analyze + run: | + composer analyze + + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + + - name: Install tools + run: | + composer install + + - name: Run Tests + run: | + composer test \ No newline at end of file diff --git a/seed/php-model/server-sent-event-examples/.gitignore b/seed/php-model/server-sent-event-examples/.gitignore new file mode 100644 index 00000000000..f38efc46ade --- /dev/null +++ b/seed/php-model/server-sent-event-examples/.gitignore @@ -0,0 +1,4 @@ +.php-cs-fixer.cache +.phpunit.result.cache +composer.lock +vendor/ \ No newline at end of file diff --git a/seed/php-model/server-sent-event-examples/.mock/definition/api.yml b/seed/php-model/server-sent-event-examples/.mock/definition/api.yml new file mode 100644 index 00000000000..80e84c41785 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/.mock/definition/api.yml @@ -0,0 +1 @@ +name: server-sent-events diff --git a/seed/php-model/server-sent-event-examples/.mock/definition/completions.yml b/seed/php-model/server-sent-event-examples/.mock/definition/completions.yml new file mode 100644 index 00000000000..09a88253331 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/.mock/definition/completions.yml @@ -0,0 +1,36 @@ +types: + StreamedCompletion: + properties: + delta: string + tokens: optional + +service: + auth: false + base-path: "" + endpoints: + stream: + method: POST + path: /stream + request: + name: StreamCompletionRequest + body: + properties: + query: string + response-stream: + type: StreamedCompletion + format: sse + terminator: "[[DONE]]" + examples: + - name: "Stream completions" + request: + query: "foo" + response: + stream: + - event: discriminant-1 + data: + delta: "foo" + tokens: 1 + - event: discriminant-2 + data: + delta: "bar" + tokens: 2 diff --git a/seed/php-model/server-sent-event-examples/.mock/fern.config.json b/seed/php-model/server-sent-event-examples/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/php-model/server-sent-event-examples/.mock/generators.yml b/seed/php-model/server-sent-event-examples/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/php-model/server-sent-event-examples/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/php-model/server-sent-event-examples/composer.json b/seed/php-model/server-sent-event-examples/composer.json new file mode 100644 index 00000000000..7f5821806d4 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/composer.json @@ -0,0 +1,40 @@ + +{ + "name": "seed/seed", + "version": "0.0.1", + "description": "Seed PHP Library", + "keywords": [ + "seed", + "api", + "sdk" + ], + "license": [], + "require": { + "php": "^8.1", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.9" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "friendsofphp/php-cs-fixer": "3.5.0", + "phpstan/phpstan": "^1.12" + }, + "autoload": { + "psr-4": { + "Seed\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "\\Seed\\Tests\\": "tests/" + } + }, + "scripts": { + "build": [ + "@php -l src", + "@php -l tests" + ], + "test": "phpunit", + "analyze": "phpstan analyze src" + } +} diff --git a/seed/php-model/server-sent-event-examples/phpstan.neon b/seed/php-model/server-sent-event-examples/phpstan.neon new file mode 100644 index 00000000000..29a11a92a19 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: max + paths: + - src + - tests \ No newline at end of file diff --git a/seed/php-model/server-sent-event-examples/phpunit.xml b/seed/php-model/server-sent-event-examples/phpunit.xml new file mode 100644 index 00000000000..54630a51163 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + \ No newline at end of file diff --git a/seed/php-model/server-sent-event-examples/snippet-templates.json b/seed/php-model/server-sent-event-examples/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/php-model/server-sent-event-examples/snippet.json b/seed/php-model/server-sent-event-examples/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/php-model/server-sent-event-examples/src/Completions/StreamedCompletion.php b/seed/php-model/server-sent-event-examples/src/Completions/StreamedCompletion.php new file mode 100644 index 00000000000..59208a9fc57 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/src/Completions/StreamedCompletion.php @@ -0,0 +1,34 @@ +delta = $values['delta']; + $this->tokens = $values['tokens'] ?? null; + } +} diff --git a/seed/php-model/server-sent-event-examples/src/Core/ArrayType.php b/seed/php-model/server-sent-event-examples/src/Core/ArrayType.php new file mode 100644 index 00000000000..b2ed8bf12b2 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/src/Core/ArrayType.php @@ -0,0 +1,16 @@ + 'valueType'] for maps, or ['valueType'] for lists + */ + public function __construct(public array $type) + { + } +} diff --git a/seed/php-model/server-sent-event-examples/src/Core/Constant.php b/seed/php-model/server-sent-event-examples/src/Core/Constant.php new file mode 100644 index 00000000000..abbac7f6649 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/src/Core/Constant.php @@ -0,0 +1,12 @@ + $type The type definition for deserialization. + * @return mixed[]|array The deserialized array. + * @throws JsonException If the decoded value is not an array. + */ + public static function decodeArray(string $json, array $type): array + { + $decoded = self::decode($json); + if (!is_array($decoded)) { + throw new JsonException("Unexpected non-array json value: " . $json); + } + return JsonDeserializer::deserializeArray($decoded, $type); + } + + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } + /** + * Decodes a JSON string and returns a mixed. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded mixed. + * @throws JsonException If the decoded value is not an mixed. + */ + public static function decodeMixed(string $json): mixed + { + return self::decode($json); + } + + /** + * Decodes a JSON string into a PHP value. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded value. + * @throws JsonException If an error occurs during JSON decoding. + */ + public static function decode(string $json): mixed + { + return json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR); + } +} diff --git a/seed/php-model/server-sent-event-examples/src/Core/JsonDeserializer.php b/seed/php-model/server-sent-event-examples/src/Core/JsonDeserializer.php new file mode 100644 index 00000000000..b1de7d141ac --- /dev/null +++ b/seed/php-model/server-sent-event-examples/src/Core/JsonDeserializer.php @@ -0,0 +1,202 @@ + $data The array to be deserialized. + * @param mixed[]|array $type The type definition from the annotation. + * @return mixed[]|array The deserialized array. + * @throws JsonException If deserialization fails. + */ + public static function deserializeArray(array $data, array $type): array + { + return Utils::isMapType($type) + ? self::deserializeMap($data, $type) + : self::deserializeList($data, $type); + } + + /** + * Deserializes a value based on its type definition. + * + * @param mixed $data The data to deserialize. + * @param mixed $type The type definition. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::deserializeUnion($data, $type); + } + + if (is_array($type)) { + return self::deserializeArray((array)$data, $type); + } + + if (gettype($type) != "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::deserializeSingleValue($data, $type); + } + + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + + /** + * Deserializes a single value based on its expected type. + * + * @param mixed $data The data to deserialize. + * @param string $type The expected type. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if ($type === 'date' && is_string($data)) { + return self::deserializeDate($data); + } + + if ($type === 'datetime' && is_string($data)) { + return self::deserializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && is_array($data)) { + return self::deserializeObject($data, $type); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP, and because + // floats make come through from json_decoded as integers + if ($type === 'float' && (is_numeric($data))) { + return (float) $data; + } + + if (gettype($data) === $type) { + return $data; + } + + 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. + * + * @param array $data The associative array to deserialize. + * @param array $type The type definition for the map. + * @return array The deserialized map. + * @throws JsonException If deserialization fails. + */ + private static function deserializeMap(array $data, array $type): array + { + $keyType = array_key_first($type); + $valueType = $type[$keyType]; + $result = []; + + foreach ($data as $key => $item) { + $key = Utils::castKey($key, (string)$keyType); + $result[$key] = self::deserializeValue($item, $valueType); + } + + return $result; + } + + /** + * Deserializes a list (indexed array) with a defined value type. + * + * @param array $data The list to deserialize. + * @param array $type The type definition for the list. + * @return array The deserialized list. + * @throws JsonException If deserialization fails. + */ + private static function deserializeList(array $data, array $type): array + { + $valueType = $type[0]; + return array_map(fn ($item) => self::deserializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-model/server-sent-event-examples/src/Core/JsonEncoder.php b/seed/php-model/server-sent-event-examples/src/Core/JsonEncoder.php new file mode 100644 index 00000000000..ba5191a8068 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/src/Core/JsonEncoder.php @@ -0,0 +1,20 @@ +format(Constant::DateFormat); + } + + /** + * Serializes a DateTime object into a string using the date-time format. + * + * @param DateTime $date The DateTime object to serialize. + * @return string The serialized date-time string. + */ + public static function serializeDateTime(DateTime $date): string + { + return $date->format(Constant::DateTimeFormat); + } + + /** + * Serializes an array based on type annotations (either a list or map). + * + * @param mixed[]|array $data The array to be serialized. + * @param mixed[]|array $type The type definition from the annotation. + * @return mixed[]|array The serialized array. + * @throws JsonException If serialization fails. + */ + public static function serializeArray(array $data, array $type): array + { + return Utils::isMapType($type) + ? self::serializeMap($data, $type) + : self::serializeList($data, $type); + } + + /** + * Serializes a value based on its type definition. + * + * @param mixed $data The value to serialize. + * @param mixed $type The type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::serializeUnion($data, $type); + } + + if (is_array($type)) { + return self::serializeArray((array)$data, $type); + } + + if (gettype($type) != "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::serializeSingleValue($data, $type); + } + + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + + /** + * Serializes a single value based on its type. + * + * @param mixed $data The value to serialize. + * @param string $type The expected type. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if (($type === 'date' || $type === 'datetime') && $data instanceof DateTime) { + return $type === 'date' ? self::serializeDate($data) : self::serializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && $data instanceof $type) { + return self::serializeObject($data); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP. + if ($type === 'float' && is_float($data)) { + return $data; + } + + if (gettype($data) === $type) { + return $data; + } + + 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. + * + * @param array $data The associative array to serialize. + * @param array $type The type definition for the map. + * @return array The serialized map. + * @throws JsonException If serialization fails. + */ + private static function serializeMap(array $data, array $type): array + { + $keyType = array_key_first($type); + if ($keyType === null) { + throw new JsonException("Unexpected no key in ArrayType."); + } + $valueType = $type[$keyType]; + $result = []; + + foreach ($data as $key => $item) { + $key = Utils::castKey($key, $keyType); + $result[$key] = self::serializeValue($item, $valueType); + } + + return $result; + } + + /** + * Serializes a list (indexed array) where only the value type is defined. + * + * @param array $data The list to serialize. + * @param array $type The type definition for the list. + * @return array The serialized list. + * @throws JsonException If serialization fails. + */ + private static function serializeList(array $data, array $type): array + { + $valueType = $type[0]; + return array_map(fn ($item) => self::serializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-model/server-sent-event-examples/src/Core/SerializableType.php b/seed/php-model/server-sent-event-examples/src/Core/SerializableType.php new file mode 100644 index 00000000000..9121bdca01c --- /dev/null +++ b/seed/php-model/server-sent-event-examples/src/Core/SerializableType.php @@ -0,0 +1,179 @@ +jsonSerialize(); + $encoded = JsonEncoder::encode($serializedObject); + if (!$encoded) { + throw new Exception("Could not encode type"); + } + return $encoded; + } + + /** + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. + */ + public function jsonSerialize(): array + { + $result = []; + $reflectionClass = new \ReflectionClass($this); + + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property); + if ($jsonKey == null) { + continue; + } + $value = $property->getValue($this); + + // Handle DateTime properties + $dateTypeAttr = $property->getAttributes(DateType::class)[0] ?? null; + if ($dateTypeAttr && $value instanceof DateTime) { + $dateType = $dateTypeAttr->newInstance()->type; + $value = ($dateType === DateType::TYPE_DATE) + ? JsonSerializer::serializeDate($value) + : JsonSerializer::serializeDateTime($value); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + + // Handle arrays with type annotations + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if ($arrayTypeAttr && is_array($value)) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonSerializer::serializeArray($value, $arrayType); + } + + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + + if ($value !== null) { + $result[$jsonKey] = $value; + } + } + + return $result; + } + + /** + * 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)); + } + return self::jsonDeserialize($decodedJson); + } + + /** + * Deserializes an array into an instance of the calling class. + * + * @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."); + } + + $args = []; + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property) ?? $property->getName(); + + if (array_key_exists($jsonKey, $data)) { + $value = $data[$jsonKey]; + + // Handle DateType annotation + $dateTypeAttr = $property->getAttributes(DateType::class)[0] ?? null; + if ($dateTypeAttr) { + $dateType = $dateTypeAttr->newInstance()->type; + if (!is_string($value)) { + throw new JsonException("Unexpected non-string type for date."); + } + $value = ($dateType === DateType::TYPE_DATE) + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); + } + + // Handle ArrayType annotation + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if (is_array($value) && $arrayTypeAttr) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonDeserializer::deserializeArray($value, $arrayType); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + + // 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 { + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; + } + } + // @phpstan-ignore-next-line + return new static($args); + } + + /** + * Retrieves the JSON key associated with a property. + * + * @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; + } +} diff --git a/seed/php-model/server-sent-event-examples/src/Core/Union.php b/seed/php-model/server-sent-event-examples/src/Core/Union.php new file mode 100644 index 00000000000..1e9fe801ee7 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/src/Core/Union.php @@ -0,0 +1,62 @@ +> The types allowed for this property, which can be strings, arrays, or nested Union types. + */ + public array $types; + + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) + { + $this->types = $types; + } + + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ + public function __toString(): string + { + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); + } +} diff --git a/seed/php-model/server-sent-event-examples/src/Core/Utils.php b/seed/php-model/server-sent-event-examples/src/Core/Utils.php new file mode 100644 index 00000000000..74416068d02 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/src/Core/Utils.php @@ -0,0 +1,61 @@ + $type The type definition from the annotation. + * @return bool True if the type is a map, false if it's a list. + */ + public static function isMapType(array $type): bool + { + return count($type) === 1 && !array_is_list($type); + } + + /** + * Casts the key to the appropriate type based on the key type. + * + * @param mixed $key The key to be cast. + * @param string $keyType The type to cast the key to ('string', 'integer', 'float'). + * @return mixed The casted key. + * @throws JsonException + */ + public static function castKey(mixed $key, string $keyType): mixed + { + if (!is_scalar($key)) { + throw new JsonException("Key must be a scalar type."); + } + return match ($keyType) { + 'integer' => (int)$key, + 'float' => (float)$key, + 'string' => (string)$key, + 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/server-sent-event-examples/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/DateArrayTypeTest.php new file mode 100644 index 00000000000..8d93afc9e44 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/DateArrayTypeTest.php @@ -0,0 +1,55 @@ +dates = $values['dates']; + } +} + +class DateArrayTypeTest extends TestCase +{ + public function testDateTimeTypesInArrays(): void + { + $data = [ + 'dates' => ['2023-01-01', '2023-02-01', '2023-03-01'] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = DateArrayType::fromJson($json); + + $this->assertInstanceOf(DateTime::class, $object->dates[0], 'dates[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dates[0]->format('Y-m-d'), 'dates[0] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[1], 'dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-02-01', $object->dates[1]->format('Y-m-d'), 'dates[1] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[2], 'dates[2] should be a DateTime instance.'); + $this->assertEquals('2023-03-01', $object->dates[2]->format('Y-m-d'), 'dates[2] should have the correct date.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for dates array.'); + } +} diff --git a/seed/php-model/server-sent-event-examples/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/EmptyArraysTest.php new file mode 100644 index 00000000000..b44f3d093e6 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/EmptyArraysTest.php @@ -0,0 +1,73 @@ + $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( + array $values, + ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; + } +} + +class EmptyArraysTest extends TestCase +{ + public function testEmptyArrays(): void + { + $data = [ + 'empty_string_array' => [], + 'empty_map_array' => [], + 'empty_dates_array' => [] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = EmptyArraysType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for EmptyArraysType.'); + + // Check that arrays are empty + $this->assertEmpty($object->emptyStringArray, 'empty_string_array should be empty.'); + $this->assertEmpty($object->emptyMapArray, 'empty_map_array should be empty.'); + $this->assertEmpty($object->emptyDatesArray, 'empty_dates_array should be empty.'); + } +} diff --git a/seed/php-model/server-sent-event-examples/tests/Seed/Core/EnumTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/EnumTest.php new file mode 100644 index 00000000000..ef5b8484dfd --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/EnumTest.php @@ -0,0 +1,76 @@ +value; + } +} + +class ShapeType extends SerializableType +{ + /** + * @var Shape $shape + */ + #[JsonProperty('shape')] + public Shape $shape; + + /** + * @var Shape[] $shapes + */ + #[ArrayType([Shape::class])] + #[JsonProperty('shapes')] + public array $shapes; + + /** + * @param Shape $shape + * @param Shape[] $shapes + */ + public function __construct( + Shape $shape, + array $shapes, + ) { + $this->shape = $shape; + $this->shapes = $shapes; + } +} + +class EnumTest extends TestCase +{ + public function testShapeEnumSerialization(): void + { + $object = new ShapeType( + Shape::Circle, + [Shape::Square, Shape::Circle, Shape::Triangle] + ); + + $expectedJson = json_encode([ + 'shape' => 'CIRCLE', + 'shapes' => ['SQUARE', 'CIRCLE', 'TRIANGLE'] + ], JSON_THROW_ON_ERROR); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString( + $expectedJson, + $serializedJson, + 'Serialized JSON does not match expected JSON for shape and shapes properties.' + ); + } +} diff --git a/seed/php-model/server-sent-event-examples/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/InvalidTypesTest.php new file mode 100644 index 00000000000..67bfd235b2f --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/InvalidTypesTest.php @@ -0,0 +1,45 @@ +integerProperty = $values['integerProperty']; + } +} + +class InvalidTypesTest extends TestCase +{ + public function testInvalidTypesThrowExceptions(): void + { + // Create test data with invalid type for integer_property (string instead of int) + $data = [ + 'integer_property' => 'not_an_integer' + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $this->expectException(\TypeError::class); + + // Attempt to deserialize invalid data + InvalidType::fromJson($json); + } +} diff --git a/seed/php-model/server-sent-event-examples/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/MixedDateArrayTypeTest.php new file mode 100644 index 00000000000..3bf18aec25b --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -0,0 +1,60 @@ + $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->mixedDates = $values['mixedDates']; + } +} + +class MixedDateArrayTypeTest extends TestCase +{ + public function testDateTimeTypesInUnionArrays(): void + { + $data = [ + 'mixed_dates' => [ + 1 => '2023-01-01T12:00:00+00:00', + 2 => null, + 3 => 'Some String' + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = MixedDateArrayType::fromJson($json); + + $this->assertInstanceOf(DateTime::class, $object->mixedDates[1], 'mixed_dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:00:00', $object->mixedDates[1]->format('Y-m-d H:i:s'), 'mixed_dates[1] should have the correct datetime.'); + + $this->assertNull($object->mixedDates[2], 'mixed_dates[2] should be null.'); + + $this->assertEquals('Some String', $object->mixedDates[3], 'mixed_dates[3] should be "Some String".'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for mixed_dates.'); + } +} diff --git a/seed/php-model/server-sent-event-examples/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/NestedUnionArrayTypeTest.php new file mode 100644 index 00000000000..4667ecafcb9 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -0,0 +1,99 @@ +nestedProperty = $values['nestedProperty']; + } +} + +class NestedUnionArrayType extends SerializableType +{ + /** + * @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( + array $values, + ) { + $this->nestedArray = $values['nestedArray']; + } +} + +class NestedUnionArrayTypeTest extends TestCase +{ + public function testNestedUnionTypesInArrays(): void + { + $data = [ + 'nested_array' => [ + 1 => [ + 1 => ['nested_property' => 'Nested One'], + 2 => null, + 4 => '2023-01-02' + ], + 2 => [ + 5 => ['nested_property' => 'Nested Two'], + 7 => '2023-02-02' + ] + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = NestedUnionArrayType::fromJson($json); + + // Level 1 + $this->assertInstanceOf(TestNestedType::class, $object->nestedArray[1][1], 'nested_array[1][1] should be an instance of TestNestedType.'); + $this->assertEquals('Nested One', $object->nestedArray[1][1]->nestedProperty, 'nested_array[1][1]->nestedProperty should match the original data.'); + + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + + // ensure dates are set with the default time + $this->assertInstanceOf(DateTime::class, $object->nestedArray[1][4], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-01-02T00:00:00+00:00', $object->nestedArray[1][4]->format(Constant::DateTimeFormat), 'nested_array[1][4] should have the correct datetime.'); + + // Level 2 + $this->assertInstanceOf(TestNestedType::class, $object->nestedArray[2][5], 'nested_array[2][5] should be an instance of TestNestedType.'); + $this->assertEquals('Nested Two', $object->nestedArray[2][5]->nestedProperty, 'nested_array[2][5]->nestedProperty should match the original data.'); + + $this->assertInstanceOf(DateTime::class, $object->nestedArray[2][7], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-02-02', $object->nestedArray[2][7]->format('Y-m-d'), 'nested_array[1][4] should have the correct date.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for nested_array.'); + } +} diff --git a/seed/php-model/server-sent-event-examples/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/NullPropertyTypeTest.php new file mode 100644 index 00000000000..134296f56e3 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/NullPropertyTypeTest.php @@ -0,0 +1,50 @@ +nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; + } +} + +class NullPropertyTypeTest extends TestCase +{ + public function testNullPropertiesAreOmitted(): void + { + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); + + $serializedObject = $object->jsonSerialize(); + + $this->assertArrayHasKey('non_null_property', $serializedObject, 'non_null_property should be present in the serialized JSON.'); + $this->assertArrayNotHasKey('null_property', $serializedObject, 'null_property should be omitted from the serialized JSON.'); + + $this->assertEquals('Test String', $serializedObject['non_null_property'], 'non_null_property should have the correct value.'); + } +} diff --git a/seed/php-model/server-sent-event-examples/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/NullableArrayTypeTest.php new file mode 100644 index 00000000000..bf6345e5c6f --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/NullableArrayTypeTest.php @@ -0,0 +1,50 @@ + $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->nullableStringArray = $values['nullableStringArray']; + } +} + +class NullableArrayTypeTest extends TestCase +{ + public function testNullableTypesInArrays(): void + { + $data = [ + 'nullable_string_array' => ['one', null, 'three'] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = NullableArrayType::fromJson($json); + + $this->assertEquals(['one', null, 'three'], $object->nullableStringArray, 'nullable_string_array should match the original data.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for nullable_string_array.'); + } +} diff --git a/seed/php-model/server-sent-event-examples/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/ScalarTypesTest.php new file mode 100644 index 00000000000..899e949836c --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/ScalarTypesTest.php @@ -0,0 +1,121 @@ + $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var array $floatArray + */ + #[ArrayType(['float'])] + #[JsonProperty('float_array')] + public array $floatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * otherFloatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * floatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ + public function __construct( + array $values, + ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->otherFloatProperty = $values['otherFloatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->floatArray = $values['floatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; + } +} + +class ScalarTypesTest extends TestCase +{ + public function testAllScalarTypesIncludingFloat(): void + { + // Create test data + $data = [ + 'integer_property' => 42, + 'float_property' => 3.14159, + 'other_float_property' => 3, + 'boolean_property' => true, + 'string_property' => 'Hello, World!', + 'int_float_array' => [1, 2.5, 3, 4.75], + 'float_array' => [1, 2, 3, 4] // ensure we handle "integer-looking" floats + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = ScalarTypesTestType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for ScalarTypesTest.'); + + // Check scalar properties + $this->assertEquals(42, $object->integerProperty, 'integer_property should be 42.'); + $this->assertEquals(3.14159, $object->floatProperty, 'float_property should be 3.14159.'); + $this->assertTrue($object->booleanProperty, 'boolean_property should be true.'); + $this->assertEquals('Hello, World!', $object->stringProperty, 'string_property should be "Hello, World!".'); + $this->assertNull($object->nullableBooleanProperty, 'nullable_boolean_property should be null.'); + + // Check int_float_array + $this->assertEquals([1, 2.5, 3, 4.75], $object->intFloatArray, 'int_float_array should match the original data.'); + } +} diff --git a/seed/php-model/server-sent-event-examples/tests/Seed/Core/TestTypeTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/TestTypeTest.php new file mode 100644 index 00000000000..8e7ca1b825c --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/TestTypeTest.php @@ -0,0 +1,201 @@ +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( + 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; + } +} + +class TestTypeTest extends TestCase +{ + /** + * Test serialization and deserialization of all types in TestType + */ + 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', + 'datetime_property' => '2023-01-01T12:34:56+00:00', + 'string_array' => ['one', 'two', 'three'], + 'map_property' => ['key1' => 1, 'key2' => 2], + 'object_array' => [ + 1 => ['nested_property' => '2021-07-20'], + 2 => null, // Testing nullable objects in array + ], + 'nested_array' => [ + 1 => [1 => 'value1', 2 => null], // Testing nullable strings in nested array + 2 => [3 => 'value3', 4 => 'value4'] + ], + 'dates_array' => ['2023-01-01', null, '2023-03-01'] // Testing nullable dates in array + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = TestType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'The serialized JSON does not match the original JSON.'); + + // Check that nullable property is null and not included in JSON + $this->assertNull($object->nullableProperty, 'Nullable property should be null.'); + // @phpstan-ignore-next-line + $this->assertFalse(array_key_exists('nullable_property', json_decode($serializedJson, true)), 'Nullable property should be omitted from JSON.'); + + // Check date properties + $this->assertInstanceOf(DateTime::class, $object->dateProperty, 'date_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dateProperty->format('Y-m-d'), 'date_property should have the correct date.'); + + $this->assertInstanceOf(DateTime::class, $object->datetimeProperty, 'datetime_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:34:56', $object->datetimeProperty->format('Y-m-d H:i:s'), 'datetime_property should have the correct datetime.'); + + // Check scalar arrays + $this->assertEquals(['one', 'two', 'three'], $object->stringArray, 'string_array should match the original data.'); + $this->assertEquals(['key1' => 1, 'key2' => 2], $object->mapProperty, 'map_property should match the original data.'); + + // Check object array with nullable elements + $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); + $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 + $this->assertEquals('value1', $object->nestedArray[1][1], 'nested_array[1][1] should match the original data.'); + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + $this->assertEquals('value3', $object->nestedArray[2][3], 'nested_array[2][3] should match the original data.'); + $this->assertEquals('value4', $object->nestedArray[2][4], 'nested_array[2][4] should match the original data.'); + + // Check dates array with nullable DateTime objects + $this->assertInstanceOf(DateTime::class, $object->datesArray[0], 'dates_array[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->datesArray[0]->format('Y-m-d'), 'dates_array[0] should have the correct date.'); + $this->assertNull($object->datesArray[1], 'dates_array[1] should be null.'); + $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.'); + } +} diff --git a/seed/php-model/server-sent-event-examples/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/UnionArrayTypeTest.php new file mode 100644 index 00000000000..8d0998f4b7e --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/UnionArrayTypeTest.php @@ -0,0 +1,56 @@ + $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->mixedArray = $values['mixedArray']; + } +} + +class UnionArrayTypeTest extends TestCase +{ + public function testUnionTypesInArrays(): void + { + $data = [ + 'mixed_array' => [ + 1 => 'one', + 2 => 2, + 3 => null + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = UnionArrayType::fromJson($json); + + $this->assertEquals('one', $object->mixedArray[1], 'mixed_array[1] should be "one".'); + $this->assertEquals(2, $object->mixedArray[2], 'mixed_array[2] should be 2.'); + $this->assertNull($object->mixedArray[3], 'mixed_array[3] should be null.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for mixed_array.'); + } +} diff --git a/seed/php-model/server-sent-event-examples/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/server-sent-event-examples/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/server-sent-event-examples/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/server-sent-events/.github/workflows/ci.yml b/seed/php-model/server-sent-events/.github/workflows/ci.yml new file mode 100644 index 00000000000..258bf33a19f --- /dev/null +++ b/seed/php-model/server-sent-events/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + + - name: Install tools + run: | + composer install + + - name: Build + run: | + composer build + + - name: Analyze + run: | + composer analyze + + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + + - name: Install tools + run: | + composer install + + - name: Run Tests + run: | + composer test \ No newline at end of file diff --git a/seed/php-model/server-sent-events/.gitignore b/seed/php-model/server-sent-events/.gitignore new file mode 100644 index 00000000000..f38efc46ade --- /dev/null +++ b/seed/php-model/server-sent-events/.gitignore @@ -0,0 +1,4 @@ +.php-cs-fixer.cache +.phpunit.result.cache +composer.lock +vendor/ \ No newline at end of file diff --git a/seed/php-model/server-sent-events/.mock/definition/api.yml b/seed/php-model/server-sent-events/.mock/definition/api.yml new file mode 100644 index 00000000000..80e84c41785 --- /dev/null +++ b/seed/php-model/server-sent-events/.mock/definition/api.yml @@ -0,0 +1 @@ +name: server-sent-events diff --git a/seed/php-model/server-sent-events/.mock/definition/completions.yml b/seed/php-model/server-sent-events/.mock/definition/completions.yml new file mode 100644 index 00000000000..d1748fad19e --- /dev/null +++ b/seed/php-model/server-sent-events/.mock/definition/completions.yml @@ -0,0 +1,22 @@ +types: + StreamedCompletion: + properties: + delta: string + tokens: optional + +service: + auth: false + base-path: "" + endpoints: + stream: + method: POST + path: /stream + request: + name: StreamCompletionRequest + body: + properties: + query: string + response-stream: + type: StreamedCompletion + format: sse + terminator: "[[DONE]]" diff --git a/seed/php-model/server-sent-events/.mock/fern.config.json b/seed/php-model/server-sent-events/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/php-model/server-sent-events/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/php-model/server-sent-events/.mock/generators.yml b/seed/php-model/server-sent-events/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/php-model/server-sent-events/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/php-model/server-sent-events/composer.json b/seed/php-model/server-sent-events/composer.json new file mode 100644 index 00000000000..7f5821806d4 --- /dev/null +++ b/seed/php-model/server-sent-events/composer.json @@ -0,0 +1,40 @@ + +{ + "name": "seed/seed", + "version": "0.0.1", + "description": "Seed PHP Library", + "keywords": [ + "seed", + "api", + "sdk" + ], + "license": [], + "require": { + "php": "^8.1", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.9" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "friendsofphp/php-cs-fixer": "3.5.0", + "phpstan/phpstan": "^1.12" + }, + "autoload": { + "psr-4": { + "Seed\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "\\Seed\\Tests\\": "tests/" + } + }, + "scripts": { + "build": [ + "@php -l src", + "@php -l tests" + ], + "test": "phpunit", + "analyze": "phpstan analyze src" + } +} diff --git a/seed/php-model/server-sent-events/phpstan.neon b/seed/php-model/server-sent-events/phpstan.neon new file mode 100644 index 00000000000..29a11a92a19 --- /dev/null +++ b/seed/php-model/server-sent-events/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: max + paths: + - src + - tests \ No newline at end of file diff --git a/seed/php-model/server-sent-events/phpunit.xml b/seed/php-model/server-sent-events/phpunit.xml new file mode 100644 index 00000000000..54630a51163 --- /dev/null +++ b/seed/php-model/server-sent-events/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + \ No newline at end of file diff --git a/seed/php-model/server-sent-events/snippet-templates.json b/seed/php-model/server-sent-events/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/php-model/server-sent-events/snippet.json b/seed/php-model/server-sent-events/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/php-model/server-sent-events/src/Completions/StreamedCompletion.php b/seed/php-model/server-sent-events/src/Completions/StreamedCompletion.php new file mode 100644 index 00000000000..59208a9fc57 --- /dev/null +++ b/seed/php-model/server-sent-events/src/Completions/StreamedCompletion.php @@ -0,0 +1,34 @@ +delta = $values['delta']; + $this->tokens = $values['tokens'] ?? null; + } +} diff --git a/seed/php-model/server-sent-events/src/Core/ArrayType.php b/seed/php-model/server-sent-events/src/Core/ArrayType.php new file mode 100644 index 00000000000..b2ed8bf12b2 --- /dev/null +++ b/seed/php-model/server-sent-events/src/Core/ArrayType.php @@ -0,0 +1,16 @@ + 'valueType'] for maps, or ['valueType'] for lists + */ + public function __construct(public array $type) + { + } +} diff --git a/seed/php-model/server-sent-events/src/Core/Constant.php b/seed/php-model/server-sent-events/src/Core/Constant.php new file mode 100644 index 00000000000..abbac7f6649 --- /dev/null +++ b/seed/php-model/server-sent-events/src/Core/Constant.php @@ -0,0 +1,12 @@ + $type The type definition for deserialization. + * @return mixed[]|array The deserialized array. + * @throws JsonException If the decoded value is not an array. + */ + public static function decodeArray(string $json, array $type): array + { + $decoded = self::decode($json); + if (!is_array($decoded)) { + throw new JsonException("Unexpected non-array json value: " . $json); + } + return JsonDeserializer::deserializeArray($decoded, $type); + } + + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } + /** + * Decodes a JSON string and returns a mixed. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded mixed. + * @throws JsonException If the decoded value is not an mixed. + */ + public static function decodeMixed(string $json): mixed + { + return self::decode($json); + } + + /** + * Decodes a JSON string into a PHP value. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded value. + * @throws JsonException If an error occurs during JSON decoding. + */ + public static function decode(string $json): mixed + { + return json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR); + } +} diff --git a/seed/php-model/server-sent-events/src/Core/JsonDeserializer.php b/seed/php-model/server-sent-events/src/Core/JsonDeserializer.php new file mode 100644 index 00000000000..b1de7d141ac --- /dev/null +++ b/seed/php-model/server-sent-events/src/Core/JsonDeserializer.php @@ -0,0 +1,202 @@ + $data The array to be deserialized. + * @param mixed[]|array $type The type definition from the annotation. + * @return mixed[]|array The deserialized array. + * @throws JsonException If deserialization fails. + */ + public static function deserializeArray(array $data, array $type): array + { + return Utils::isMapType($type) + ? self::deserializeMap($data, $type) + : self::deserializeList($data, $type); + } + + /** + * Deserializes a value based on its type definition. + * + * @param mixed $data The data to deserialize. + * @param mixed $type The type definition. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::deserializeUnion($data, $type); + } + + if (is_array($type)) { + return self::deserializeArray((array)$data, $type); + } + + if (gettype($type) != "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::deserializeSingleValue($data, $type); + } + + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + + /** + * Deserializes a single value based on its expected type. + * + * @param mixed $data The data to deserialize. + * @param string $type The expected type. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if ($type === 'date' && is_string($data)) { + return self::deserializeDate($data); + } + + if ($type === 'datetime' && is_string($data)) { + return self::deserializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && is_array($data)) { + return self::deserializeObject($data, $type); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP, and because + // floats make come through from json_decoded as integers + if ($type === 'float' && (is_numeric($data))) { + return (float) $data; + } + + if (gettype($data) === $type) { + return $data; + } + + 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. + * + * @param array $data The associative array to deserialize. + * @param array $type The type definition for the map. + * @return array The deserialized map. + * @throws JsonException If deserialization fails. + */ + private static function deserializeMap(array $data, array $type): array + { + $keyType = array_key_first($type); + $valueType = $type[$keyType]; + $result = []; + + foreach ($data as $key => $item) { + $key = Utils::castKey($key, (string)$keyType); + $result[$key] = self::deserializeValue($item, $valueType); + } + + return $result; + } + + /** + * Deserializes a list (indexed array) with a defined value type. + * + * @param array $data The list to deserialize. + * @param array $type The type definition for the list. + * @return array The deserialized list. + * @throws JsonException If deserialization fails. + */ + private static function deserializeList(array $data, array $type): array + { + $valueType = $type[0]; + return array_map(fn ($item) => self::deserializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-model/server-sent-events/src/Core/JsonEncoder.php b/seed/php-model/server-sent-events/src/Core/JsonEncoder.php new file mode 100644 index 00000000000..ba5191a8068 --- /dev/null +++ b/seed/php-model/server-sent-events/src/Core/JsonEncoder.php @@ -0,0 +1,20 @@ +format(Constant::DateFormat); + } + + /** + * Serializes a DateTime object into a string using the date-time format. + * + * @param DateTime $date The DateTime object to serialize. + * @return string The serialized date-time string. + */ + public static function serializeDateTime(DateTime $date): string + { + return $date->format(Constant::DateTimeFormat); + } + + /** + * Serializes an array based on type annotations (either a list or map). + * + * @param mixed[]|array $data The array to be serialized. + * @param mixed[]|array $type The type definition from the annotation. + * @return mixed[]|array The serialized array. + * @throws JsonException If serialization fails. + */ + public static function serializeArray(array $data, array $type): array + { + return Utils::isMapType($type) + ? self::serializeMap($data, $type) + : self::serializeList($data, $type); + } + + /** + * Serializes a value based on its type definition. + * + * @param mixed $data The value to serialize. + * @param mixed $type The type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::serializeUnion($data, $type); + } + + if (is_array($type)) { + return self::serializeArray((array)$data, $type); + } + + if (gettype($type) != "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::serializeSingleValue($data, $type); + } + + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + + /** + * Serializes a single value based on its type. + * + * @param mixed $data The value to serialize. + * @param string $type The expected type. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if (($type === 'date' || $type === 'datetime') && $data instanceof DateTime) { + return $type === 'date' ? self::serializeDate($data) : self::serializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && $data instanceof $type) { + return self::serializeObject($data); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP. + if ($type === 'float' && is_float($data)) { + return $data; + } + + if (gettype($data) === $type) { + return $data; + } + + 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. + * + * @param array $data The associative array to serialize. + * @param array $type The type definition for the map. + * @return array The serialized map. + * @throws JsonException If serialization fails. + */ + private static function serializeMap(array $data, array $type): array + { + $keyType = array_key_first($type); + if ($keyType === null) { + throw new JsonException("Unexpected no key in ArrayType."); + } + $valueType = $type[$keyType]; + $result = []; + + foreach ($data as $key => $item) { + $key = Utils::castKey($key, $keyType); + $result[$key] = self::serializeValue($item, $valueType); + } + + return $result; + } + + /** + * Serializes a list (indexed array) where only the value type is defined. + * + * @param array $data The list to serialize. + * @param array $type The type definition for the list. + * @return array The serialized list. + * @throws JsonException If serialization fails. + */ + private static function serializeList(array $data, array $type): array + { + $valueType = $type[0]; + return array_map(fn ($item) => self::serializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-model/server-sent-events/src/Core/SerializableType.php b/seed/php-model/server-sent-events/src/Core/SerializableType.php new file mode 100644 index 00000000000..9121bdca01c --- /dev/null +++ b/seed/php-model/server-sent-events/src/Core/SerializableType.php @@ -0,0 +1,179 @@ +jsonSerialize(); + $encoded = JsonEncoder::encode($serializedObject); + if (!$encoded) { + throw new Exception("Could not encode type"); + } + return $encoded; + } + + /** + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. + */ + public function jsonSerialize(): array + { + $result = []; + $reflectionClass = new \ReflectionClass($this); + + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property); + if ($jsonKey == null) { + continue; + } + $value = $property->getValue($this); + + // Handle DateTime properties + $dateTypeAttr = $property->getAttributes(DateType::class)[0] ?? null; + if ($dateTypeAttr && $value instanceof DateTime) { + $dateType = $dateTypeAttr->newInstance()->type; + $value = ($dateType === DateType::TYPE_DATE) + ? JsonSerializer::serializeDate($value) + : JsonSerializer::serializeDateTime($value); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + + // Handle arrays with type annotations + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if ($arrayTypeAttr && is_array($value)) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonSerializer::serializeArray($value, $arrayType); + } + + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + + if ($value !== null) { + $result[$jsonKey] = $value; + } + } + + return $result; + } + + /** + * 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)); + } + return self::jsonDeserialize($decodedJson); + } + + /** + * Deserializes an array into an instance of the calling class. + * + * @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."); + } + + $args = []; + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property) ?? $property->getName(); + + if (array_key_exists($jsonKey, $data)) { + $value = $data[$jsonKey]; + + // Handle DateType annotation + $dateTypeAttr = $property->getAttributes(DateType::class)[0] ?? null; + if ($dateTypeAttr) { + $dateType = $dateTypeAttr->newInstance()->type; + if (!is_string($value)) { + throw new JsonException("Unexpected non-string type for date."); + } + $value = ($dateType === DateType::TYPE_DATE) + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); + } + + // Handle ArrayType annotation + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if (is_array($value) && $arrayTypeAttr) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonDeserializer::deserializeArray($value, $arrayType); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + + // 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 { + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; + } + } + // @phpstan-ignore-next-line + return new static($args); + } + + /** + * Retrieves the JSON key associated with a property. + * + * @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; + } +} diff --git a/seed/php-model/server-sent-events/src/Core/Union.php b/seed/php-model/server-sent-events/src/Core/Union.php new file mode 100644 index 00000000000..1e9fe801ee7 --- /dev/null +++ b/seed/php-model/server-sent-events/src/Core/Union.php @@ -0,0 +1,62 @@ +> The types allowed for this property, which can be strings, arrays, or nested Union types. + */ + public array $types; + + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) + { + $this->types = $types; + } + + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ + public function __toString(): string + { + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); + } +} diff --git a/seed/php-model/server-sent-events/src/Core/Utils.php b/seed/php-model/server-sent-events/src/Core/Utils.php new file mode 100644 index 00000000000..74416068d02 --- /dev/null +++ b/seed/php-model/server-sent-events/src/Core/Utils.php @@ -0,0 +1,61 @@ + $type The type definition from the annotation. + * @return bool True if the type is a map, false if it's a list. + */ + public static function isMapType(array $type): bool + { + return count($type) === 1 && !array_is_list($type); + } + + /** + * Casts the key to the appropriate type based on the key type. + * + * @param mixed $key The key to be cast. + * @param string $keyType The type to cast the key to ('string', 'integer', 'float'). + * @return mixed The casted key. + * @throws JsonException + */ + public static function castKey(mixed $key, string $keyType): mixed + { + if (!is_scalar($key)) { + throw new JsonException("Key must be a scalar type."); + } + return match ($keyType) { + 'integer' => (int)$key, + 'float' => (float)$key, + 'string' => (string)$key, + 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/server-sent-events/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/DateArrayTypeTest.php new file mode 100644 index 00000000000..8d93afc9e44 --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/DateArrayTypeTest.php @@ -0,0 +1,55 @@ +dates = $values['dates']; + } +} + +class DateArrayTypeTest extends TestCase +{ + public function testDateTimeTypesInArrays(): void + { + $data = [ + 'dates' => ['2023-01-01', '2023-02-01', '2023-03-01'] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = DateArrayType::fromJson($json); + + $this->assertInstanceOf(DateTime::class, $object->dates[0], 'dates[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dates[0]->format('Y-m-d'), 'dates[0] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[1], 'dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-02-01', $object->dates[1]->format('Y-m-d'), 'dates[1] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[2], 'dates[2] should be a DateTime instance.'); + $this->assertEquals('2023-03-01', $object->dates[2]->format('Y-m-d'), 'dates[2] should have the correct date.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for dates array.'); + } +} diff --git a/seed/php-model/server-sent-events/tests/Seed/Core/EmptyArraysTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/EmptyArraysTest.php new file mode 100644 index 00000000000..b44f3d093e6 --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/EmptyArraysTest.php @@ -0,0 +1,73 @@ + $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( + array $values, + ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; + } +} + +class EmptyArraysTest extends TestCase +{ + public function testEmptyArrays(): void + { + $data = [ + 'empty_string_array' => [], + 'empty_map_array' => [], + 'empty_dates_array' => [] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = EmptyArraysType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for EmptyArraysType.'); + + // Check that arrays are empty + $this->assertEmpty($object->emptyStringArray, 'empty_string_array should be empty.'); + $this->assertEmpty($object->emptyMapArray, 'empty_map_array should be empty.'); + $this->assertEmpty($object->emptyDatesArray, 'empty_dates_array should be empty.'); + } +} diff --git a/seed/php-model/server-sent-events/tests/Seed/Core/EnumTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/EnumTest.php new file mode 100644 index 00000000000..ef5b8484dfd --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/EnumTest.php @@ -0,0 +1,76 @@ +value; + } +} + +class ShapeType extends SerializableType +{ + /** + * @var Shape $shape + */ + #[JsonProperty('shape')] + public Shape $shape; + + /** + * @var Shape[] $shapes + */ + #[ArrayType([Shape::class])] + #[JsonProperty('shapes')] + public array $shapes; + + /** + * @param Shape $shape + * @param Shape[] $shapes + */ + public function __construct( + Shape $shape, + array $shapes, + ) { + $this->shape = $shape; + $this->shapes = $shapes; + } +} + +class EnumTest extends TestCase +{ + public function testShapeEnumSerialization(): void + { + $object = new ShapeType( + Shape::Circle, + [Shape::Square, Shape::Circle, Shape::Triangle] + ); + + $expectedJson = json_encode([ + 'shape' => 'CIRCLE', + 'shapes' => ['SQUARE', 'CIRCLE', 'TRIANGLE'] + ], JSON_THROW_ON_ERROR); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString( + $expectedJson, + $serializedJson, + 'Serialized JSON does not match expected JSON for shape and shapes properties.' + ); + } +} diff --git a/seed/php-model/server-sent-events/tests/Seed/Core/InvalidTypesTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/InvalidTypesTest.php new file mode 100644 index 00000000000..67bfd235b2f --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/InvalidTypesTest.php @@ -0,0 +1,45 @@ +integerProperty = $values['integerProperty']; + } +} + +class InvalidTypesTest extends TestCase +{ + public function testInvalidTypesThrowExceptions(): void + { + // Create test data with invalid type for integer_property (string instead of int) + $data = [ + 'integer_property' => 'not_an_integer' + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $this->expectException(\TypeError::class); + + // Attempt to deserialize invalid data + InvalidType::fromJson($json); + } +} diff --git a/seed/php-model/server-sent-events/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/MixedDateArrayTypeTest.php new file mode 100644 index 00000000000..3bf18aec25b --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -0,0 +1,60 @@ + $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->mixedDates = $values['mixedDates']; + } +} + +class MixedDateArrayTypeTest extends TestCase +{ + public function testDateTimeTypesInUnionArrays(): void + { + $data = [ + 'mixed_dates' => [ + 1 => '2023-01-01T12:00:00+00:00', + 2 => null, + 3 => 'Some String' + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = MixedDateArrayType::fromJson($json); + + $this->assertInstanceOf(DateTime::class, $object->mixedDates[1], 'mixed_dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:00:00', $object->mixedDates[1]->format('Y-m-d H:i:s'), 'mixed_dates[1] should have the correct datetime.'); + + $this->assertNull($object->mixedDates[2], 'mixed_dates[2] should be null.'); + + $this->assertEquals('Some String', $object->mixedDates[3], 'mixed_dates[3] should be "Some String".'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for mixed_dates.'); + } +} diff --git a/seed/php-model/server-sent-events/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/NestedUnionArrayTypeTest.php new file mode 100644 index 00000000000..4667ecafcb9 --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -0,0 +1,99 @@ +nestedProperty = $values['nestedProperty']; + } +} + +class NestedUnionArrayType extends SerializableType +{ + /** + * @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( + array $values, + ) { + $this->nestedArray = $values['nestedArray']; + } +} + +class NestedUnionArrayTypeTest extends TestCase +{ + public function testNestedUnionTypesInArrays(): void + { + $data = [ + 'nested_array' => [ + 1 => [ + 1 => ['nested_property' => 'Nested One'], + 2 => null, + 4 => '2023-01-02' + ], + 2 => [ + 5 => ['nested_property' => 'Nested Two'], + 7 => '2023-02-02' + ] + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = NestedUnionArrayType::fromJson($json); + + // Level 1 + $this->assertInstanceOf(TestNestedType::class, $object->nestedArray[1][1], 'nested_array[1][1] should be an instance of TestNestedType.'); + $this->assertEquals('Nested One', $object->nestedArray[1][1]->nestedProperty, 'nested_array[1][1]->nestedProperty should match the original data.'); + + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + + // ensure dates are set with the default time + $this->assertInstanceOf(DateTime::class, $object->nestedArray[1][4], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-01-02T00:00:00+00:00', $object->nestedArray[1][4]->format(Constant::DateTimeFormat), 'nested_array[1][4] should have the correct datetime.'); + + // Level 2 + $this->assertInstanceOf(TestNestedType::class, $object->nestedArray[2][5], 'nested_array[2][5] should be an instance of TestNestedType.'); + $this->assertEquals('Nested Two', $object->nestedArray[2][5]->nestedProperty, 'nested_array[2][5]->nestedProperty should match the original data.'); + + $this->assertInstanceOf(DateTime::class, $object->nestedArray[2][7], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-02-02', $object->nestedArray[2][7]->format('Y-m-d'), 'nested_array[1][4] should have the correct date.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for nested_array.'); + } +} diff --git a/seed/php-model/server-sent-events/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/NullPropertyTypeTest.php new file mode 100644 index 00000000000..134296f56e3 --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/NullPropertyTypeTest.php @@ -0,0 +1,50 @@ +nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; + } +} + +class NullPropertyTypeTest extends TestCase +{ + public function testNullPropertiesAreOmitted(): void + { + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); + + $serializedObject = $object->jsonSerialize(); + + $this->assertArrayHasKey('non_null_property', $serializedObject, 'non_null_property should be present in the serialized JSON.'); + $this->assertArrayNotHasKey('null_property', $serializedObject, 'null_property should be omitted from the serialized JSON.'); + + $this->assertEquals('Test String', $serializedObject['non_null_property'], 'non_null_property should have the correct value.'); + } +} diff --git a/seed/php-model/server-sent-events/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/NullableArrayTypeTest.php new file mode 100644 index 00000000000..bf6345e5c6f --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/NullableArrayTypeTest.php @@ -0,0 +1,50 @@ + $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->nullableStringArray = $values['nullableStringArray']; + } +} + +class NullableArrayTypeTest extends TestCase +{ + public function testNullableTypesInArrays(): void + { + $data = [ + 'nullable_string_array' => ['one', null, 'three'] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = NullableArrayType::fromJson($json); + + $this->assertEquals(['one', null, 'three'], $object->nullableStringArray, 'nullable_string_array should match the original data.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for nullable_string_array.'); + } +} diff --git a/seed/php-model/server-sent-events/tests/Seed/Core/ScalarTypesTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/ScalarTypesTest.php new file mode 100644 index 00000000000..899e949836c --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/ScalarTypesTest.php @@ -0,0 +1,121 @@ + $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var array $floatArray + */ + #[ArrayType(['float'])] + #[JsonProperty('float_array')] + public array $floatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * otherFloatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * floatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ + public function __construct( + array $values, + ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->otherFloatProperty = $values['otherFloatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->floatArray = $values['floatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; + } +} + +class ScalarTypesTest extends TestCase +{ + public function testAllScalarTypesIncludingFloat(): void + { + // Create test data + $data = [ + 'integer_property' => 42, + 'float_property' => 3.14159, + 'other_float_property' => 3, + 'boolean_property' => true, + 'string_property' => 'Hello, World!', + 'int_float_array' => [1, 2.5, 3, 4.75], + 'float_array' => [1, 2, 3, 4] // ensure we handle "integer-looking" floats + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = ScalarTypesTestType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for ScalarTypesTest.'); + + // Check scalar properties + $this->assertEquals(42, $object->integerProperty, 'integer_property should be 42.'); + $this->assertEquals(3.14159, $object->floatProperty, 'float_property should be 3.14159.'); + $this->assertTrue($object->booleanProperty, 'boolean_property should be true.'); + $this->assertEquals('Hello, World!', $object->stringProperty, 'string_property should be "Hello, World!".'); + $this->assertNull($object->nullableBooleanProperty, 'nullable_boolean_property should be null.'); + + // Check int_float_array + $this->assertEquals([1, 2.5, 3, 4.75], $object->intFloatArray, 'int_float_array should match the original data.'); + } +} diff --git a/seed/php-model/server-sent-events/tests/Seed/Core/TestTypeTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/TestTypeTest.php new file mode 100644 index 00000000000..8e7ca1b825c --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/TestTypeTest.php @@ -0,0 +1,201 @@ +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( + 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; + } +} + +class TestTypeTest extends TestCase +{ + /** + * Test serialization and deserialization of all types in TestType + */ + 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', + 'datetime_property' => '2023-01-01T12:34:56+00:00', + 'string_array' => ['one', 'two', 'three'], + 'map_property' => ['key1' => 1, 'key2' => 2], + 'object_array' => [ + 1 => ['nested_property' => '2021-07-20'], + 2 => null, // Testing nullable objects in array + ], + 'nested_array' => [ + 1 => [1 => 'value1', 2 => null], // Testing nullable strings in nested array + 2 => [3 => 'value3', 4 => 'value4'] + ], + 'dates_array' => ['2023-01-01', null, '2023-03-01'] // Testing nullable dates in array + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = TestType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'The serialized JSON does not match the original JSON.'); + + // Check that nullable property is null and not included in JSON + $this->assertNull($object->nullableProperty, 'Nullable property should be null.'); + // @phpstan-ignore-next-line + $this->assertFalse(array_key_exists('nullable_property', json_decode($serializedJson, true)), 'Nullable property should be omitted from JSON.'); + + // Check date properties + $this->assertInstanceOf(DateTime::class, $object->dateProperty, 'date_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dateProperty->format('Y-m-d'), 'date_property should have the correct date.'); + + $this->assertInstanceOf(DateTime::class, $object->datetimeProperty, 'datetime_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:34:56', $object->datetimeProperty->format('Y-m-d H:i:s'), 'datetime_property should have the correct datetime.'); + + // Check scalar arrays + $this->assertEquals(['one', 'two', 'three'], $object->stringArray, 'string_array should match the original data.'); + $this->assertEquals(['key1' => 1, 'key2' => 2], $object->mapProperty, 'map_property should match the original data.'); + + // Check object array with nullable elements + $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); + $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 + $this->assertEquals('value1', $object->nestedArray[1][1], 'nested_array[1][1] should match the original data.'); + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + $this->assertEquals('value3', $object->nestedArray[2][3], 'nested_array[2][3] should match the original data.'); + $this->assertEquals('value4', $object->nestedArray[2][4], 'nested_array[2][4] should match the original data.'); + + // Check dates array with nullable DateTime objects + $this->assertInstanceOf(DateTime::class, $object->datesArray[0], 'dates_array[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->datesArray[0]->format('Y-m-d'), 'dates_array[0] should have the correct date.'); + $this->assertNull($object->datesArray[1], 'dates_array[1] should be null.'); + $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.'); + } +} diff --git a/seed/php-model/server-sent-events/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/UnionArrayTypeTest.php new file mode 100644 index 00000000000..8d0998f4b7e --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/UnionArrayTypeTest.php @@ -0,0 +1,56 @@ + $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->mixedArray = $values['mixedArray']; + } +} + +class UnionArrayTypeTest extends TestCase +{ + public function testUnionTypesInArrays(): void + { + $data = [ + 'mixed_array' => [ + 1 => 'one', + 2 => 2, + 3 => null + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = UnionArrayType::fromJson($json); + + $this->assertEquals('one', $object->mixedArray[1], 'mixed_array[1] should be "one".'); + $this->assertEquals(2, $object->mixedArray[2], 'mixed_array[2] should be 2.'); + $this->assertNull($object->mixedArray[3], 'mixed_array[3] should be null.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for mixed_array.'); + } +} diff --git a/seed/php-model/server-sent-events/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/server-sent-events/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/server-sent-events/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/simple-fhir/src/BaseResource.php b/seed/php-model/simple-fhir/src/BaseResource.php index fe335daac3b..ea9ceafd1db 100644 --- a/seed/php-model/simple-fhir/src/BaseResource.php +++ b/seed/php-model/simple-fhir/src/BaseResource.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class BaseResource extends SerializableType { @@ -15,9 +16,9 @@ class BaseResource extends SerializableType public string $id; /** - * @var array $relatedResources + * @var array $relatedResources */ - #[JsonProperty('related_resources'), ArrayType(['mixed'])] + #[JsonProperty('related_resources'), ArrayType([new Union(Account::class, Patient::class, Practitioner::class, Script::class)])] public array $relatedResources; /** @@ -29,7 +30,7 @@ class BaseResource extends SerializableType /** * @param array{ * id: string, - * relatedResources: array, + * relatedResources: array, * memo: Memo, * } $values */ diff --git a/seed/php-model/simple-fhir/src/Core/JsonDecoder.php b/seed/php-model/simple-fhir/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/simple-fhir/src/Core/JsonDecoder.php +++ b/seed/php-model/simple-fhir/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php b/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php +++ b/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/simple-fhir/src/Core/JsonSerializer.php b/seed/php-model/simple-fhir/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/simple-fhir/src/Core/JsonSerializer.php +++ b/seed/php-model/simple-fhir/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/simple-fhir/src/Core/SerializableType.php b/seed/php-model/simple-fhir/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/simple-fhir/src/Core/SerializableType.php +++ b/seed/php-model/simple-fhir/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/simple-fhir/src/Core/Union.php b/seed/php-model/simple-fhir/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/simple-fhir/src/Core/Union.php +++ b/seed/php-model/simple-fhir/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/single-url-environment-default/src/Core/JsonDecoder.php b/seed/php-model/single-url-environment-default/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/single-url-environment-default/src/Core/JsonDecoder.php +++ b/seed/php-model/single-url-environment-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/single-url-environment-no-default/src/Core/JsonDecoder.php b/seed/php-model/single-url-environment-no-default/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/single-url-environment-no-default/src/Core/JsonDecoder.php +++ b/seed/php-model/single-url-environment-no-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/streaming-parameter/src/Core/JsonDecoder.php b/seed/php-model/streaming-parameter/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/streaming-parameter/src/Core/JsonDecoder.php +++ b/seed/php-model/streaming-parameter/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php b/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php +++ b/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php b/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php +++ b/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/streaming-parameter/src/Core/SerializableType.php b/seed/php-model/streaming-parameter/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/streaming-parameter/src/Core/SerializableType.php +++ b/seed/php-model/streaming-parameter/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/streaming-parameter/src/Core/Union.php b/seed/php-model/streaming-parameter/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/streaming-parameter/src/Core/Union.php +++ b/seed/php-model/streaming-parameter/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/streaming/src/Core/JsonDecoder.php b/seed/php-model/streaming/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/streaming/src/Core/JsonDecoder.php +++ b/seed/php-model/streaming/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/streaming/src/Core/JsonDeserializer.php b/seed/php-model/streaming/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/streaming/src/Core/JsonDeserializer.php +++ b/seed/php-model/streaming/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/streaming/src/Core/JsonSerializer.php b/seed/php-model/streaming/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/streaming/src/Core/JsonSerializer.php +++ b/seed/php-model/streaming/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/streaming/src/Core/SerializableType.php b/seed/php-model/streaming/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/streaming/src/Core/SerializableType.php +++ b/seed/php-model/streaming/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/streaming/src/Core/Union.php b/seed/php-model/streaming/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/streaming/src/Core/Union.php +++ b/seed/php-model/streaming/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/streaming/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/streaming/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/streaming/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/trace/src/Core/JsonDecoder.php b/seed/php-model/trace/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/trace/src/Core/JsonDecoder.php +++ b/seed/php-model/trace/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/trace/src/Core/JsonDeserializer.php b/seed/php-model/trace/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/trace/src/Core/JsonDeserializer.php +++ b/seed/php-model/trace/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/trace/src/Core/JsonSerializer.php b/seed/php-model/trace/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/trace/src/Core/JsonSerializer.php +++ b/seed/php-model/trace/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/trace/src/Core/SerializableType.php b/seed/php-model/trace/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/trace/src/Core/SerializableType.php +++ b/seed/php-model/trace/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/trace/src/Core/Union.php b/seed/php-model/trace/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/trace/src/Core/Union.php +++ b/seed/php-model/trace/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/trace/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/trace/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/trace/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/undiscriminated-unions/src/Core/JsonDecoder.php b/seed/php-model/undiscriminated-unions/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/JsonDecoder.php +++ b/seed/php-model/undiscriminated-unions/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php b/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php +++ b/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php b/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php +++ b/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php b/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php +++ b/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/undiscriminated-unions/src/Core/Union.php b/seed/php-model/undiscriminated-unions/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/Union.php +++ b/seed/php-model/undiscriminated-unions/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/unions/src/Core/JsonDecoder.php b/seed/php-model/unions/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/unions/src/Core/JsonDecoder.php +++ b/seed/php-model/unions/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/unions/src/Core/JsonDeserializer.php b/seed/php-model/unions/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/unions/src/Core/JsonDeserializer.php +++ b/seed/php-model/unions/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/unions/src/Core/JsonSerializer.php b/seed/php-model/unions/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/unions/src/Core/JsonSerializer.php +++ b/seed/php-model/unions/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/unions/src/Core/SerializableType.php b/seed/php-model/unions/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/unions/src/Core/SerializableType.php +++ b/seed/php-model/unions/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/unions/src/Core/Union.php b/seed/php-model/unions/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/unions/src/Core/Union.php +++ b/seed/php-model/unions/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/unions/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/unions/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/unions/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/unknown/src/Core/JsonDecoder.php b/seed/php-model/unknown/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/unknown/src/Core/JsonDecoder.php +++ b/seed/php-model/unknown/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/unknown/src/Core/JsonDeserializer.php b/seed/php-model/unknown/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/unknown/src/Core/JsonDeserializer.php +++ b/seed/php-model/unknown/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/unknown/src/Core/JsonSerializer.php b/seed/php-model/unknown/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/unknown/src/Core/JsonSerializer.php +++ b/seed/php-model/unknown/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/unknown/src/Core/SerializableType.php b/seed/php-model/unknown/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/unknown/src/Core/SerializableType.php +++ b/seed/php-model/unknown/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/unknown/src/Core/Union.php b/seed/php-model/unknown/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/unknown/src/Core/Union.php +++ b/seed/php-model/unknown/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/unknown/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/unknown/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/unknown/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/validation/src/Core/JsonDecoder.php b/seed/php-model/validation/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/validation/src/Core/JsonDecoder.php +++ b/seed/php-model/validation/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/validation/src/Core/JsonDeserializer.php b/seed/php-model/validation/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/validation/src/Core/JsonDeserializer.php +++ b/seed/php-model/validation/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/validation/src/Core/JsonSerializer.php b/seed/php-model/validation/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/validation/src/Core/JsonSerializer.php +++ b/seed/php-model/validation/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/validation/src/Core/SerializableType.php b/seed/php-model/validation/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/validation/src/Core/SerializableType.php +++ b/seed/php-model/validation/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/validation/src/Core/Union.php b/seed/php-model/validation/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/validation/src/Core/Union.php +++ b/seed/php-model/validation/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/validation/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/validation/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/validation/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/variables/src/Core/JsonDecoder.php b/seed/php-model/variables/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/variables/src/Core/JsonDecoder.php +++ b/seed/php-model/variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/variables/src/Core/JsonDeserializer.php b/seed/php-model/variables/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/variables/src/Core/JsonDeserializer.php +++ b/seed/php-model/variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/variables/src/Core/JsonSerializer.php b/seed/php-model/variables/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/variables/src/Core/JsonSerializer.php +++ b/seed/php-model/variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/variables/src/Core/SerializableType.php b/seed/php-model/variables/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/variables/src/Core/SerializableType.php +++ b/seed/php-model/variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/variables/src/Core/Union.php b/seed/php-model/variables/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/variables/src/Core/Union.php +++ b/seed/php-model/variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/version-no-default/src/Core/JsonDecoder.php b/seed/php-model/version-no-default/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/version-no-default/src/Core/JsonDecoder.php +++ b/seed/php-model/version-no-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/version-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-model/version-no-default/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-model/version-no-default/src/Core/JsonSerializer.php +++ b/seed/php-model/version-no-default/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/version-no-default/src/Core/SerializableType.php +++ b/seed/php-model/version-no-default/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/version-no-default/src/Core/Union.php +++ b/seed/php-model/version-no-default/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/version/src/Core/JsonDecoder.php b/seed/php-model/version/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/version/src/Core/JsonDecoder.php +++ b/seed/php-model/version/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/version/src/Core/JsonDeserializer.php b/seed/php-model/version/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/version/src/Core/JsonDeserializer.php +++ b/seed/php-model/version/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/version/src/Core/JsonSerializer.php b/seed/php-model/version/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/version/src/Core/JsonSerializer.php +++ b/seed/php-model/version/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/version/src/Core/SerializableType.php b/seed/php-model/version/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/version/src/Core/SerializableType.php +++ b/seed/php-model/version/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/version/src/Core/Union.php b/seed/php-model/version/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/version/src/Core/Union.php +++ b/seed/php-model/version/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/version/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/version/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/version/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/websocket/src/Core/JsonDecoder.php b/seed/php-model/websocket/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-model/websocket/src/Core/JsonDecoder.php +++ b/seed/php-model/websocket/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/websocket/src/Core/JsonDeserializer.php b/seed/php-model/websocket/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-model/websocket/src/Core/JsonDeserializer.php +++ b/seed/php-model/websocket/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/websocket/src/Core/JsonSerializer.php b/seed/php-model/websocket/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-model/websocket/src/Core/JsonSerializer.php +++ b/seed/php-model/websocket/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/websocket/src/Core/SerializableType.php b/seed/php-model/websocket/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-model/websocket/src/Core/SerializableType.php +++ b/seed/php-model/websocket/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/websocket/src/Core/Union.php b/seed/php-model/websocket/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-model/websocket/src/Core/Union.php +++ b/seed/php-model/websocket/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/websocket/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/websocket/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-model/websocket/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/alias-extends/src/Core/JsonDecoder.php b/seed/php-sdk/alias-extends/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/alias-extends/src/Core/JsonDecoder.php +++ b/seed/php-sdk/alias-extends/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php b/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php b/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php +++ b/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/alias-extends/src/Core/SerializableType.php b/seed/php-sdk/alias-extends/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/alias-extends/src/Core/SerializableType.php +++ b/seed/php-sdk/alias-extends/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/alias-extends/src/Core/Union.php b/seed/php-sdk/alias-extends/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/alias-extends/src/Core/Union.php +++ b/seed/php-sdk/alias-extends/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/alias/src/Core/JsonDecoder.php b/seed/php-sdk/alias/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/alias/src/Core/JsonDecoder.php +++ b/seed/php-sdk/alias/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/alias/src/Core/JsonDeserializer.php b/seed/php-sdk/alias/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/alias/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/alias/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/alias/src/Core/JsonSerializer.php b/seed/php-sdk/alias/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/alias/src/Core/JsonSerializer.php +++ b/seed/php-sdk/alias/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/alias/src/Core/SerializableType.php b/seed/php-sdk/alias/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/alias/src/Core/SerializableType.php +++ b/seed/php-sdk/alias/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/alias/src/Core/Union.php b/seed/php-sdk/alias/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/alias/src/Core/Union.php +++ b/seed/php-sdk/alias/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/alias/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/alias/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/alias/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/any-auth/src/Core/JsonDecoder.php b/seed/php-sdk/any-auth/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/any-auth/src/Core/JsonDecoder.php +++ b/seed/php-sdk/any-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php b/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/any-auth/src/Core/JsonSerializer.php b/seed/php-sdk/any-auth/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/any-auth/src/Core/JsonSerializer.php +++ b/seed/php-sdk/any-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/any-auth/src/Core/SerializableType.php b/seed/php-sdk/any-auth/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/any-auth/src/Core/SerializableType.php +++ b/seed/php-sdk/any-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/any-auth/src/Core/Union.php b/seed/php-sdk/any-auth/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/any-auth/src/Core/Union.php +++ b/seed/php-sdk/any-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/api-wide-base-path/src/Core/JsonDecoder.php b/seed/php-sdk/api-wide-base-path/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/api-wide-base-path/src/Core/JsonDecoder.php +++ b/seed/php-sdk/api-wide-base-path/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/audiences/src/Core/JsonDecoder.php b/seed/php-sdk/audiences/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/audiences/src/Core/JsonDecoder.php +++ b/seed/php-sdk/audiences/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/audiences/src/Core/JsonDeserializer.php b/seed/php-sdk/audiences/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/audiences/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/audiences/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/audiences/src/Core/JsonSerializer.php b/seed/php-sdk/audiences/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/audiences/src/Core/JsonSerializer.php +++ b/seed/php-sdk/audiences/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/audiences/src/Core/SerializableType.php b/seed/php-sdk/audiences/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/audiences/src/Core/SerializableType.php +++ b/seed/php-sdk/audiences/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/audiences/src/Core/Union.php b/seed/php-sdk/audiences/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/audiences/src/Core/Union.php +++ b/seed/php-sdk/audiences/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/audiences/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/audiences/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/audiences/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/auth-environment-variables/src/Core/JsonDecoder.php b/seed/php-sdk/auth-environment-variables/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/SerializableType.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/Union.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDecoder.php b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/basic-auth/src/Core/JsonDecoder.php b/seed/php-sdk/basic-auth/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/basic-auth/src/Core/JsonDecoder.php +++ b/seed/php-sdk/basic-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php b/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php b/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php +++ b/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/basic-auth/src/Core/SerializableType.php b/seed/php-sdk/basic-auth/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/basic-auth/src/Core/SerializableType.php +++ b/seed/php-sdk/basic-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/basic-auth/src/Core/Union.php b/seed/php-sdk/basic-auth/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/basic-auth/src/Core/Union.php +++ b/seed/php-sdk/basic-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDecoder.php b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDecoder.php +++ b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/bytes/src/Core/JsonDecoder.php b/seed/php-sdk/bytes/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/bytes/src/Core/JsonDecoder.php +++ b/seed/php-sdk/bytes/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/bytes/src/Core/JsonDeserializer.php b/seed/php-sdk/bytes/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/bytes/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/bytes/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/bytes/src/Core/JsonSerializer.php b/seed/php-sdk/bytes/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/bytes/src/Core/JsonSerializer.php +++ b/seed/php-sdk/bytes/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/bytes/src/Core/SerializableType.php b/seed/php-sdk/bytes/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/bytes/src/Core/SerializableType.php +++ b/seed/php-sdk/bytes/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/bytes/src/Core/Union.php b/seed/php-sdk/bytes/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/bytes/src/Core/Union.php +++ b/seed/php-sdk/bytes/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/bytes/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/bytes/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/bytes/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/circular-references-advanced/src/Core/JsonDecoder.php b/seed/php-sdk/circular-references-advanced/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/JsonDecoder.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/JsonSerializer.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/SerializableType.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/Union.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/circular-references/src/Core/JsonDecoder.php b/seed/php-sdk/circular-references/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/circular-references/src/Core/JsonDecoder.php +++ b/seed/php-sdk/circular-references/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php b/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/circular-references/src/Core/JsonSerializer.php b/seed/php-sdk/circular-references/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/circular-references/src/Core/JsonSerializer.php +++ b/seed/php-sdk/circular-references/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/circular-references/src/Core/SerializableType.php b/seed/php-sdk/circular-references/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/circular-references/src/Core/SerializableType.php +++ b/seed/php-sdk/circular-references/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/circular-references/src/Core/Union.php b/seed/php-sdk/circular-references/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/circular-references/src/Core/Union.php +++ b/seed/php-sdk/circular-references/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/cross-package-type-names/src/Core/JsonDecoder.php b/seed/php-sdk/cross-package-type-names/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/cross-package-type-names/src/Core/JsonDecoder.php +++ b/seed/php-sdk/cross-package-type-names/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/custom-auth/src/Core/JsonDecoder.php b/seed/php-sdk/custom-auth/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/custom-auth/src/Core/JsonDecoder.php +++ b/seed/php-sdk/custom-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php b/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php b/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php +++ b/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/custom-auth/src/Core/SerializableType.php b/seed/php-sdk/custom-auth/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/custom-auth/src/Core/SerializableType.php +++ b/seed/php-sdk/custom-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/custom-auth/src/Core/Union.php b/seed/php-sdk/custom-auth/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/custom-auth/src/Core/Union.php +++ b/seed/php-sdk/custom-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/enum/src/Core/JsonDecoder.php b/seed/php-sdk/enum/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/enum/src/Core/JsonDecoder.php +++ b/seed/php-sdk/enum/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/enum/src/Core/JsonDeserializer.php b/seed/php-sdk/enum/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/enum/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/enum/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/enum/src/Core/JsonSerializer.php b/seed/php-sdk/enum/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/enum/src/Core/JsonSerializer.php +++ b/seed/php-sdk/enum/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/enum/src/Core/SerializableType.php b/seed/php-sdk/enum/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/enum/src/Core/SerializableType.php +++ b/seed/php-sdk/enum/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/enum/src/Core/Union.php b/seed/php-sdk/enum/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/enum/src/Core/Union.php +++ b/seed/php-sdk/enum/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/enum/src/InlinedRequest/Requests/SendEnumInlinedRequest.php b/seed/php-sdk/enum/src/InlinedRequest/Requests/SendEnumInlinedRequest.php index 55b714944c7..eca5e93593c 100644 --- a/seed/php-sdk/enum/src/InlinedRequest/Requests/SendEnumInlinedRequest.php +++ b/seed/php-sdk/enum/src/InlinedRequest/Requests/SendEnumInlinedRequest.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Types\Operand; use Seed\Core\JsonProperty; +use Seed\Types\Color; class SendEnumInlinedRequest extends SerializableType { @@ -21,23 +22,23 @@ class SendEnumInlinedRequest extends SerializableType public ?string $maybeOperand; /** - * @var mixed $operandOrColor + * @var value-of|value-of $operandOrColor */ #[JsonProperty('operandOrColor')] - public mixed $operandOrColor; + public string $operandOrColor; /** - * @var mixed $maybeOperandOrColor + * @var value-of|value-of|null $maybeOperandOrColor */ #[JsonProperty('maybeOperandOrColor')] - public mixed $maybeOperandOrColor; + public string|null $maybeOperandOrColor; /** * @param array{ * operand: value-of, * maybeOperand?: ?value-of, - * operandOrColor: mixed, - * maybeOperandOrColor: mixed, + * operandOrColor: value-of|value-of, + * maybeOperandOrColor?: value-of|value-of|null, * } $values */ public function __construct( @@ -46,6 +47,6 @@ public function __construct( $this->operand = $values['operand']; $this->maybeOperand = $values['maybeOperand'] ?? null; $this->operandOrColor = $values['operandOrColor']; - $this->maybeOperandOrColor = $values['maybeOperandOrColor']; + $this->maybeOperandOrColor = $values['maybeOperandOrColor'] ?? null; } } diff --git a/seed/php-sdk/enum/src/PathParam/PathParamClient.php b/seed/php-sdk/enum/src/PathParam/PathParamClient.php index 621b9958a18..99bacb44266 100644 --- a/seed/php-sdk/enum/src/PathParam/PathParamClient.php +++ b/seed/php-sdk/enum/src/PathParam/PathParamClient.php @@ -4,6 +4,7 @@ use Seed\Core\RawClient; use Seed\Types\Operand; +use Seed\Types\Color; use Seed\Exceptions\SeedException; use Seed\Exceptions\SeedApiException; use Seed\Core\JsonApiRequest; @@ -29,15 +30,15 @@ public function __construct( /** * @param value-of $operand * @param ?value-of $maybeOperand - * @param mixed $operandOrColor - * @param mixed $maybeOperandOrColor + * @param value-of|value-of $operandOrColor + * @param value-of|value-of|null $maybeOperandOrColor * @param ?array{ * baseUrl?: string, * } $options * @throws SeedException * @throws SeedApiException */ - public function send(string $operand, ?string $maybeOperand = null, mixed $operandOrColor, mixed $maybeOperandOrColor, ?array $options = null): void + public function send(string $operand, ?string $maybeOperand = null, string $operandOrColor, string|null $maybeOperandOrColor = null, ?array $options = null): void { try { $response = $this->client->sendRequest( diff --git a/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumAsQueryParamRequest.php b/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumAsQueryParamRequest.php index b026f8a66f1..d73c5650fb0 100644 --- a/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumAsQueryParamRequest.php +++ b/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumAsQueryParamRequest.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Types\Operand; +use Seed\Types\Color; class SendEnumAsQueryParamRequest extends SerializableType { @@ -18,21 +19,21 @@ class SendEnumAsQueryParamRequest extends SerializableType public ?string $maybeOperand; /** - * @var mixed $operandOrColor + * @var value-of|value-of $operandOrColor */ - public mixed $operandOrColor; + public string $operandOrColor; /** - * @var mixed $maybeOperandOrColor + * @var value-of|value-of|null $maybeOperandOrColor */ - public mixed $maybeOperandOrColor; + public string|null $maybeOperandOrColor; /** * @param array{ * operand: value-of, * maybeOperand?: ?value-of, - * operandOrColor: mixed, - * maybeOperandOrColor: mixed, + * operandOrColor: value-of|value-of, + * maybeOperandOrColor?: value-of|value-of|null, * } $values */ public function __construct( @@ -41,6 +42,6 @@ public function __construct( $this->operand = $values['operand']; $this->maybeOperand = $values['maybeOperand'] ?? null; $this->operandOrColor = $values['operandOrColor']; - $this->maybeOperandOrColor = $values['maybeOperandOrColor']; + $this->maybeOperandOrColor = $values['maybeOperandOrColor'] ?? null; } } diff --git a/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumListAsQueryParamRequest.php b/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumListAsQueryParamRequest.php index 5acfab2c4e7..b7c0a49cde0 100644 --- a/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumListAsQueryParamRequest.php +++ b/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumListAsQueryParamRequest.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Types\Operand; +use Seed\Types\Color; class SendEnumListAsQueryParamRequest extends SerializableType { @@ -18,12 +19,12 @@ class SendEnumListAsQueryParamRequest extends SerializableType public array $maybeOperand; /** - * @var array $operandOrColor + * @var array|value-of> $operandOrColor */ public array $operandOrColor; /** - * @var array $maybeOperandOrColor + * @var array|value-of|null> $maybeOperandOrColor */ public array $maybeOperandOrColor; @@ -31,8 +32,8 @@ class SendEnumListAsQueryParamRequest extends SerializableType * @param array{ * operand: array>, * maybeOperand: array>, - * operandOrColor: array, - * maybeOperandOrColor: array, + * operandOrColor: array|value-of>, + * maybeOperandOrColor: array|value-of|null>, * } $values */ public function __construct( diff --git a/seed/php-sdk/enum/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/enum/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/enum/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/error-property/src/Core/JsonDecoder.php b/seed/php-sdk/error-property/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/error-property/src/Core/JsonDecoder.php +++ b/seed/php-sdk/error-property/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/error-property/src/Core/JsonDeserializer.php b/seed/php-sdk/error-property/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/error-property/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/error-property/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/error-property/src/Core/JsonSerializer.php b/seed/php-sdk/error-property/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/error-property/src/Core/JsonSerializer.php +++ b/seed/php-sdk/error-property/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/error-property/src/Core/SerializableType.php b/seed/php-sdk/error-property/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/error-property/src/Core/SerializableType.php +++ b/seed/php-sdk/error-property/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/error-property/src/Core/Union.php b/seed/php-sdk/error-property/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/error-property/src/Core/Union.php +++ b/seed/php-sdk/error-property/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/error-property/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/error-property/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/error-property/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/examples/src/Core/JsonDecoder.php b/seed/php-sdk/examples/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/examples/src/Core/JsonDecoder.php +++ b/seed/php-sdk/examples/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/examples/src/Core/JsonDeserializer.php b/seed/php-sdk/examples/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/examples/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/examples/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/examples/src/Core/JsonSerializer.php b/seed/php-sdk/examples/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/examples/src/Core/JsonSerializer.php +++ b/seed/php-sdk/examples/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/examples/src/Core/SerializableType.php b/seed/php-sdk/examples/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/examples/src/Core/SerializableType.php +++ b/seed/php-sdk/examples/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/examples/src/Core/Union.php b/seed/php-sdk/examples/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/examples/src/Core/Union.php +++ b/seed/php-sdk/examples/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/examples/src/Types/Identifier.php b/seed/php-sdk/examples/src/Types/Identifier.php index 891355cc1f7..efcb8cc8274 100644 --- a/seed/php-sdk/examples/src/Types/Identifier.php +++ b/seed/php-sdk/examples/src/Types/Identifier.php @@ -8,10 +8,10 @@ class Identifier extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @var string $value @@ -27,7 +27,7 @@ class Identifier extends SerializableType /** * @param array{ - * type: mixed, + * type: value-of|value-of, * value: string, * label: string, * } $values diff --git a/seed/php-sdk/examples/src/Types/Types/Entity.php b/seed/php-sdk/examples/src/Types/Types/Entity.php index 7316bb6b399..830aef6b2f4 100644 --- a/seed/php-sdk/examples/src/Types/Types/Entity.php +++ b/seed/php-sdk/examples/src/Types/Types/Entity.php @@ -3,15 +3,17 @@ namespace Seed\Types\Types; use Seed\Core\SerializableType; +use Seed\Types\BasicType; +use Seed\Types\ComplexType; use Seed\Core\JsonProperty; class Entity extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @var string $name @@ -21,7 +23,7 @@ class Entity extends SerializableType /** * @param array{ - * type: mixed, + * type: value-of|value-of, * name: string, * } $values */ diff --git a/seed/php-sdk/examples/src/Types/Types/ResponseType.php b/seed/php-sdk/examples/src/Types/Types/ResponseType.php index 48d032f4eb5..71faed3ceea 100644 --- a/seed/php-sdk/examples/src/Types/Types/ResponseType.php +++ b/seed/php-sdk/examples/src/Types/Types/ResponseType.php @@ -3,19 +3,21 @@ namespace Seed\Types\Types; use Seed\Core\SerializableType; +use Seed\Types\BasicType; +use Seed\Types\ComplexType; use Seed\Core\JsonProperty; class ResponseType extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @param array{ - * type: mixed, + * type: value-of|value-of, * } $values */ public function __construct( diff --git a/seed/php-sdk/examples/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/examples/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/examples/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/exhaustive/src/Core/JsonDecoder.php b/seed/php-sdk/exhaustive/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/exhaustive/src/Core/JsonDecoder.php +++ b/seed/php-sdk/exhaustive/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php b/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php b/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/exhaustive/src/Core/SerializableType.php b/seed/php-sdk/exhaustive/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/exhaustive/src/Core/SerializableType.php +++ b/seed/php-sdk/exhaustive/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/exhaustive/src/Core/Union.php b/seed/php-sdk/exhaustive/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/exhaustive/src/Core/Union.php +++ b/seed/php-sdk/exhaustive/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/extends/src/Core/JsonDecoder.php b/seed/php-sdk/extends/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/extends/src/Core/JsonDecoder.php +++ b/seed/php-sdk/extends/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/extends/src/Core/JsonDeserializer.php b/seed/php-sdk/extends/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/extends/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/extends/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/extends/src/Core/JsonSerializer.php b/seed/php-sdk/extends/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/extends/src/Core/JsonSerializer.php +++ b/seed/php-sdk/extends/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/extends/src/Core/SerializableType.php b/seed/php-sdk/extends/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/extends/src/Core/SerializableType.php +++ b/seed/php-sdk/extends/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/extends/src/Core/Union.php b/seed/php-sdk/extends/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/extends/src/Core/Union.php +++ b/seed/php-sdk/extends/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/extends/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/extends/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/extends/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/extra-properties/src/Core/JsonDecoder.php b/seed/php-sdk/extra-properties/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/extra-properties/src/Core/JsonDecoder.php +++ b/seed/php-sdk/extra-properties/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php b/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php b/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php +++ b/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/extra-properties/src/Core/SerializableType.php b/seed/php-sdk/extra-properties/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/extra-properties/src/Core/SerializableType.php +++ b/seed/php-sdk/extra-properties/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/extra-properties/src/Core/Union.php b/seed/php-sdk/extra-properties/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/extra-properties/src/Core/Union.php +++ b/seed/php-sdk/extra-properties/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/file-download/src/Core/JsonDecoder.php b/seed/php-sdk/file-download/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/file-download/src/Core/JsonDecoder.php +++ b/seed/php-sdk/file-download/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/file-download/src/Core/JsonDeserializer.php b/seed/php-sdk/file-download/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/file-download/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/file-download/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/file-download/src/Core/JsonSerializer.php b/seed/php-sdk/file-download/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/file-download/src/Core/JsonSerializer.php +++ b/seed/php-sdk/file-download/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/file-download/src/Core/SerializableType.php b/seed/php-sdk/file-download/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/file-download/src/Core/SerializableType.php +++ b/seed/php-sdk/file-download/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/file-download/src/Core/Union.php b/seed/php-sdk/file-download/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/file-download/src/Core/Union.php +++ b/seed/php-sdk/file-download/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/file-download/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/file-download/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/file-download/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/file-upload/src/Core/JsonDecoder.php b/seed/php-sdk/file-upload/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/file-upload/src/Core/JsonDecoder.php +++ b/seed/php-sdk/file-upload/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php b/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/file-upload/src/Core/JsonSerializer.php b/seed/php-sdk/file-upload/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/file-upload/src/Core/JsonSerializer.php +++ b/seed/php-sdk/file-upload/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/file-upload/src/Core/SerializableType.php b/seed/php-sdk/file-upload/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/file-upload/src/Core/SerializableType.php +++ b/seed/php-sdk/file-upload/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/file-upload/src/Core/Union.php b/seed/php-sdk/file-upload/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/file-upload/src/Core/Union.php +++ b/seed/php-sdk/file-upload/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/folders/src/Core/JsonDecoder.php b/seed/php-sdk/folders/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/folders/src/Core/JsonDecoder.php +++ b/seed/php-sdk/folders/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/folders/src/Core/JsonDeserializer.php b/seed/php-sdk/folders/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/folders/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/folders/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/folders/src/Core/JsonSerializer.php b/seed/php-sdk/folders/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/folders/src/Core/JsonSerializer.php +++ b/seed/php-sdk/folders/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/folders/src/Core/SerializableType.php b/seed/php-sdk/folders/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/folders/src/Core/SerializableType.php +++ b/seed/php-sdk/folders/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/folders/src/Core/Union.php b/seed/php-sdk/folders/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/folders/src/Core/Union.php +++ b/seed/php-sdk/folders/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/folders/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/folders/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/folders/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDecoder.php b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDecoder.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/SerializableType.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/Union.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php index ebd3d19a264..2882f1b0477 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class DeleteRequest extends SerializableType { @@ -27,25 +28,25 @@ class DeleteRequest extends SerializableType public ?string $namespace; /** - * @var mixed $filter + * @var array|array|null $filter */ - #[JsonProperty('filter')] - public mixed $filter; + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $filter; /** * @param array{ * ids?: ?array, * deleteAll?: ?bool, * namespace?: ?string, - * filter: mixed, + * filter?: array|array|null, * } $values */ public function __construct( - array $values, + array $values = [], ) { $this->ids = $values['ids'] ?? null; $this->deleteAll = $values['deleteAll'] ?? null; $this->namespace = $values['namespace'] ?? null; - $this->filter = $values['filter']; + $this->filter = $values['filter'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php index 7219ecab76c..0706e7ce2ff 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php @@ -4,23 +4,24 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; +use Seed\Core\Union; class DescribeRequest extends SerializableType { /** - * @var mixed $filter + * @var array|array|null $filter */ - #[JsonProperty('filter')] - public mixed $filter; + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $filter; /** * @param array{ - * filter: mixed, + * filter?: array|array|null, * } $values */ public function __construct( - array $values, + array $values = [], ) { - $this->filter = $values['filter']; + $this->filter = $values['filter'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php index 9fd063c8cff..88f2351d064 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; +use Seed\Core\Union; use Seed\Types\QueryColumn; use Seed\Core\ArrayType; use Seed\Types\IndexedData; @@ -23,10 +24,10 @@ class QueryRequest extends SerializableType public int $topK; /** - * @var mixed $filter + * @var array|array|null $filter */ - #[JsonProperty('filter')] - public mixed $filter; + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $filter; /** * @var ?bool $includeValues @@ -68,7 +69,7 @@ class QueryRequest extends SerializableType * @param array{ * namespace?: ?string, * topK: int, - * filter: mixed, + * filter?: array|array|null, * includeValues?: ?bool, * includeMetadata?: ?bool, * queries?: ?array, @@ -82,7 +83,7 @@ public function __construct( ) { $this->namespace = $values['namespace'] ?? null; $this->topK = $values['topK']; - $this->filter = $values['filter']; + $this->filter = $values['filter'] ?? null; $this->includeValues = $values['includeValues'] ?? null; $this->includeMetadata = $values['includeMetadata'] ?? null; $this->queries = $values['queries'] ?? null; diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php index 7e2afca26af..21a4c59b468 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; use Seed\Types\IndexedData; class UpdateRequest extends SerializableType @@ -22,10 +23,10 @@ class UpdateRequest extends SerializableType public ?array $values; /** - * @var mixed $setMetadata + * @var array|array|null $setMetadata */ - #[JsonProperty('setMetadata')] - public mixed $setMetadata; + #[JsonProperty('setMetadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $setMetadata; /** * @var ?string $namespace @@ -43,7 +44,7 @@ class UpdateRequest extends SerializableType * @param array{ * id: string, * values?: ?array, - * setMetadata: mixed, + * setMetadata?: array|array|null, * namespace?: ?string, * indexedData?: ?IndexedData, * } $values @@ -53,7 +54,7 @@ public function __construct( ) { $this->id = $values['id']; $this->values = $values['values'] ?? null; - $this->setMetadata = $values['setMetadata']; + $this->setMetadata = $values['setMetadata'] ?? null; $this->namespace = $values['namespace'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php b/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php index c5e2615ac05..c696c3d3bc3 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class Column extends SerializableType { @@ -21,10 +22,10 @@ class Column extends SerializableType public array $values; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @var ?IndexedData $indexedData @@ -36,7 +37,7 @@ class Column extends SerializableType * @param array{ * id: string, * values: array, - * metadata: mixed, + * metadata?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -45,7 +46,7 @@ public function __construct( ) { $this->id = $values['id']; $this->values = $values['values']; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php b/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php index 9e23e5acb30..27e8ae75d26 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class QueryColumn extends SerializableType { @@ -27,10 +28,10 @@ class QueryColumn extends SerializableType public ?string $namespace; /** - * @var mixed $filter + * @var array|array|null $filter */ - #[JsonProperty('filter')] - public mixed $filter; + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $filter; /** * @var ?IndexedData $indexedData @@ -43,7 +44,7 @@ class QueryColumn extends SerializableType * values: array, * topK?: ?int, * namespace?: ?string, - * filter: mixed, + * filter?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -53,7 +54,7 @@ public function __construct( $this->values = $values['values']; $this->topK = $values['topK'] ?? null; $this->namespace = $values['namespace'] ?? null; - $this->filter = $values['filter']; + $this->filter = $values['filter'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php b/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php index e770ca3b3e9..9c0399df337 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class ScoredColumn extends SerializableType { @@ -27,10 +28,10 @@ class ScoredColumn extends SerializableType public ?array $values; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @var ?IndexedData $indexedData @@ -43,7 +44,7 @@ class ScoredColumn extends SerializableType * id: string, * score?: ?float, * values?: ?array, - * metadata: mixed, + * metadata?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -53,7 +54,7 @@ public function __construct( $this->id = $values['id']; $this->score = $values['score'] ?? null; $this->values = $values['values'] ?? null; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/grpc-proto/src/Core/JsonDecoder.php b/seed/php-sdk/grpc-proto/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/grpc-proto/src/Core/JsonDecoder.php +++ b/seed/php-sdk/grpc-proto/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php b/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php b/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php +++ b/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/grpc-proto/src/Core/SerializableType.php b/seed/php-sdk/grpc-proto/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/grpc-proto/src/Core/SerializableType.php +++ b/seed/php-sdk/grpc-proto/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/grpc-proto/src/Core/Union.php b/seed/php-sdk/grpc-proto/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/grpc-proto/src/Core/Union.php +++ b/seed/php-sdk/grpc-proto/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/grpc-proto/src/Types/UserModel.php b/seed/php-sdk/grpc-proto/src/Types/UserModel.php index aa7089f7af2..cff03d8ec8c 100644 --- a/seed/php-sdk/grpc-proto/src/Types/UserModel.php +++ b/seed/php-sdk/grpc-proto/src/Types/UserModel.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; +use Seed\Core\Union; class UserModel extends SerializableType { @@ -32,10 +33,10 @@ class UserModel extends SerializableType public ?float $weight; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @param array{ @@ -43,16 +44,16 @@ class UserModel extends SerializableType * email?: ?string, * age?: ?int, * weight?: ?float, - * metadata: mixed, + * metadata?: array|array|null, * } $values */ public function __construct( - array $values, + array $values = [], ) { $this->username = $values['username'] ?? null; $this->email = $values['email'] ?? null; $this->age = $values['age'] ?? null; $this->weight = $values['weight'] ?? null; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php b/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php index ba5b06a57f3..a239f9835bc 100644 --- a/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php +++ b/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; +use Seed\Core\Union; class CreateRequest extends SerializableType { @@ -32,10 +33,10 @@ class CreateRequest extends SerializableType public ?float $weight; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @param array{ @@ -43,16 +44,16 @@ class CreateRequest extends SerializableType * email?: ?string, * age?: ?int, * weight?: ?float, - * metadata: mixed, + * metadata?: array|array|null, * } $values */ public function __construct( - array $values, + array $values = [], ) { $this->username = $values['username'] ?? null; $this->email = $values['email'] ?? null; $this->age = $values['age'] ?? null; $this->weight = $values['weight'] ?? null; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/idempotency-headers/src/Core/JsonDecoder.php b/seed/php-sdk/idempotency-headers/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/JsonDecoder.php +++ b/seed/php-sdk/idempotency-headers/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php b/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php b/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php +++ b/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php b/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php +++ b/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/idempotency-headers/src/Core/Union.php b/seed/php-sdk/idempotency-headers/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/Union.php +++ b/seed/php-sdk/idempotency-headers/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/imdb/src/Core/JsonDecoder.php b/seed/php-sdk/imdb/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/imdb/src/Core/JsonDecoder.php +++ b/seed/php-sdk/imdb/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/imdb/src/Core/JsonDeserializer.php b/seed/php-sdk/imdb/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/imdb/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/imdb/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/imdb/src/Core/JsonSerializer.php b/seed/php-sdk/imdb/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/imdb/src/Core/JsonSerializer.php +++ b/seed/php-sdk/imdb/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/imdb/src/Core/SerializableType.php b/seed/php-sdk/imdb/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/imdb/src/Core/SerializableType.php +++ b/seed/php-sdk/imdb/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/imdb/src/Core/Union.php b/seed/php-sdk/imdb/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/imdb/src/Core/Union.php +++ b/seed/php-sdk/imdb/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/imdb/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/imdb/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/imdb/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/literal/src/Core/JsonDecoder.php b/seed/php-sdk/literal/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/literal/src/Core/JsonDecoder.php +++ b/seed/php-sdk/literal/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/literal/src/Core/JsonDeserializer.php b/seed/php-sdk/literal/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/literal/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/literal/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/literal/src/Core/JsonSerializer.php b/seed/php-sdk/literal/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/literal/src/Core/JsonSerializer.php +++ b/seed/php-sdk/literal/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/literal/src/Core/SerializableType.php b/seed/php-sdk/literal/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/literal/src/Core/SerializableType.php +++ b/seed/php-sdk/literal/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/literal/src/Core/Union.php b/seed/php-sdk/literal/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/literal/src/Core/Union.php +++ b/seed/php-sdk/literal/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/literal/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/literal/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/literal/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/mixed-case/src/Core/JsonDecoder.php b/seed/php-sdk/mixed-case/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/mixed-case/src/Core/JsonDecoder.php +++ b/seed/php-sdk/mixed-case/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php b/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php b/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php +++ b/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/mixed-case/src/Core/SerializableType.php b/seed/php-sdk/mixed-case/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/mixed-case/src/Core/SerializableType.php +++ b/seed/php-sdk/mixed-case/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/mixed-case/src/Core/Union.php b/seed/php-sdk/mixed-case/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/mixed-case/src/Core/Union.php +++ b/seed/php-sdk/mixed-case/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/mixed-file-directory/src/Core/JsonDecoder.php b/seed/php-sdk/mixed-file-directory/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/JsonDecoder.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/JsonSerializer.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/SerializableType.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/Union.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/multi-line-docs/src/Core/JsonDecoder.php b/seed/php-sdk/multi-line-docs/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/JsonDecoder.php +++ b/seed/php-sdk/multi-line-docs/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/multi-line-docs/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/JsonSerializer.php +++ b/seed/php-sdk/multi-line-docs/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/SerializableType.php +++ b/seed/php-sdk/multi-line-docs/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/Union.php +++ b/seed/php-sdk/multi-line-docs/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/no-environment/src/Core/JsonDecoder.php b/seed/php-sdk/no-environment/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/no-environment/src/Core/JsonDecoder.php +++ b/seed/php-sdk/no-environment/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php b/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/no-environment/src/Core/JsonSerializer.php b/seed/php-sdk/no-environment/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/no-environment/src/Core/JsonSerializer.php +++ b/seed/php-sdk/no-environment/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/no-environment/src/Core/SerializableType.php b/seed/php-sdk/no-environment/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/no-environment/src/Core/SerializableType.php +++ b/seed/php-sdk/no-environment/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/no-environment/src/Core/Union.php b/seed/php-sdk/no-environment/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/no-environment/src/Core/Union.php +++ b/seed/php-sdk/no-environment/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDecoder.php b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDecoder.php +++ b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/oauth-client-credentials/src/Core/JsonDecoder.php b/seed/php-sdk/oauth-client-credentials/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/JsonDecoder.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/JsonSerializer.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/SerializableType.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/Union.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/object/src/Core/JsonDecoder.php b/seed/php-sdk/object/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/object/src/Core/JsonDecoder.php +++ b/seed/php-sdk/object/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/object/src/Core/JsonDeserializer.php b/seed/php-sdk/object/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/object/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/object/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/object/src/Core/JsonSerializer.php b/seed/php-sdk/object/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/object/src/Core/JsonSerializer.php +++ b/seed/php-sdk/object/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/object/src/Core/SerializableType.php b/seed/php-sdk/object/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/object/src/Core/SerializableType.php +++ b/seed/php-sdk/object/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/object/src/Core/Union.php b/seed/php-sdk/object/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/object/src/Core/Union.php +++ b/seed/php-sdk/object/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/object/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/object/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/object/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/objects-with-imports/src/Core/JsonDecoder.php b/seed/php-sdk/objects-with-imports/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/JsonDecoder.php +++ b/seed/php-sdk/objects-with-imports/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/objects-with-imports/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/JsonSerializer.php +++ b/seed/php-sdk/objects-with-imports/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/SerializableType.php +++ b/seed/php-sdk/objects-with-imports/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/Union.php +++ b/seed/php-sdk/objects-with-imports/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/optional/src/Core/JsonDecoder.php b/seed/php-sdk/optional/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/optional/src/Core/JsonDecoder.php +++ b/seed/php-sdk/optional/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/optional/src/Core/JsonDeserializer.php b/seed/php-sdk/optional/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/optional/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/optional/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/optional/src/Core/JsonSerializer.php b/seed/php-sdk/optional/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/optional/src/Core/JsonSerializer.php +++ b/seed/php-sdk/optional/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/optional/src/Core/SerializableType.php b/seed/php-sdk/optional/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/optional/src/Core/SerializableType.php +++ b/seed/php-sdk/optional/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/optional/src/Core/Union.php b/seed/php-sdk/optional/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/optional/src/Core/Union.php +++ b/seed/php-sdk/optional/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/optional/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/optional/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/optional/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/package-yml/src/Core/JsonDecoder.php b/seed/php-sdk/package-yml/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/package-yml/src/Core/JsonDecoder.php +++ b/seed/php-sdk/package-yml/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php b/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/package-yml/src/Core/JsonSerializer.php b/seed/php-sdk/package-yml/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/package-yml/src/Core/JsonSerializer.php +++ b/seed/php-sdk/package-yml/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/package-yml/src/Core/SerializableType.php b/seed/php-sdk/package-yml/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/package-yml/src/Core/SerializableType.php +++ b/seed/php-sdk/package-yml/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/package-yml/src/Core/Union.php b/seed/php-sdk/package-yml/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/package-yml/src/Core/Union.php +++ b/seed/php-sdk/package-yml/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/pagination/src/Core/JsonDecoder.php b/seed/php-sdk/pagination/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/pagination/src/Core/JsonDecoder.php +++ b/seed/php-sdk/pagination/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/pagination/src/Core/JsonDeserializer.php b/seed/php-sdk/pagination/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/pagination/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/pagination/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/pagination/src/Core/JsonSerializer.php b/seed/php-sdk/pagination/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/pagination/src/Core/JsonSerializer.php +++ b/seed/php-sdk/pagination/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/pagination/src/Core/SerializableType.php b/seed/php-sdk/pagination/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/pagination/src/Core/SerializableType.php +++ b/seed/php-sdk/pagination/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/pagination/src/Core/Union.php b/seed/php-sdk/pagination/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/pagination/src/Core/Union.php +++ b/seed/php-sdk/pagination/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/pagination/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/pagination/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/pagination/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/plain-text/src/Core/JsonDecoder.php b/seed/php-sdk/plain-text/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/plain-text/src/Core/JsonDecoder.php +++ b/seed/php-sdk/plain-text/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php b/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/plain-text/src/Core/JsonSerializer.php b/seed/php-sdk/plain-text/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/plain-text/src/Core/JsonSerializer.php +++ b/seed/php-sdk/plain-text/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/plain-text/src/Core/SerializableType.php b/seed/php-sdk/plain-text/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/plain-text/src/Core/SerializableType.php +++ b/seed/php-sdk/plain-text/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/plain-text/src/Core/Union.php b/seed/php-sdk/plain-text/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/plain-text/src/Core/Union.php +++ b/seed/php-sdk/plain-text/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/query-parameters/src/Core/JsonDecoder.php b/seed/php-sdk/query-parameters/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/query-parameters/src/Core/JsonDecoder.php +++ b/seed/php-sdk/query-parameters/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php b/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php b/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php +++ b/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/query-parameters/src/Core/SerializableType.php b/seed/php-sdk/query-parameters/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/query-parameters/src/Core/SerializableType.php +++ b/seed/php-sdk/query-parameters/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/query-parameters/src/Core/Union.php b/seed/php-sdk/query-parameters/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/query-parameters/src/Core/Union.php +++ b/seed/php-sdk/query-parameters/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/reserved-keywords/src/Core/JsonDecoder.php b/seed/php-sdk/reserved-keywords/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/JsonDecoder.php +++ b/seed/php-sdk/reserved-keywords/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php b/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php b/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php +++ b/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php b/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php +++ b/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/reserved-keywords/src/Core/Union.php b/seed/php-sdk/reserved-keywords/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/Union.php +++ b/seed/php-sdk/reserved-keywords/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/response-property/src/Core/JsonDecoder.php b/seed/php-sdk/response-property/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/response-property/src/Core/JsonDecoder.php +++ b/seed/php-sdk/response-property/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/response-property/src/Core/JsonDeserializer.php b/seed/php-sdk/response-property/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/response-property/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/response-property/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/response-property/src/Core/JsonSerializer.php b/seed/php-sdk/response-property/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/response-property/src/Core/JsonSerializer.php +++ b/seed/php-sdk/response-property/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/response-property/src/Core/SerializableType.php b/seed/php-sdk/response-property/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/response-property/src/Core/SerializableType.php +++ b/seed/php-sdk/response-property/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/response-property/src/Core/Union.php b/seed/php-sdk/response-property/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/response-property/src/Core/Union.php +++ b/seed/php-sdk/response-property/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/response-property/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/response-property/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/response-property/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/.github/workflows/ci.yml b/seed/php-sdk/server-sent-event-examples/.github/workflows/ci.yml new file mode 100644 index 00000000000..258bf33a19f --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + + - name: Install tools + run: | + composer install + + - name: Build + run: | + composer build + + - name: Analyze + run: | + composer analyze + + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + + - name: Install tools + run: | + composer install + + - name: Run Tests + run: | + composer test \ No newline at end of file diff --git a/seed/php-sdk/server-sent-event-examples/.gitignore b/seed/php-sdk/server-sent-event-examples/.gitignore new file mode 100644 index 00000000000..f38efc46ade --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/.gitignore @@ -0,0 +1,4 @@ +.php-cs-fixer.cache +.phpunit.result.cache +composer.lock +vendor/ \ No newline at end of file diff --git a/seed/php-sdk/server-sent-event-examples/.mock/definition/api.yml b/seed/php-sdk/server-sent-event-examples/.mock/definition/api.yml new file mode 100644 index 00000000000..80e84c41785 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/.mock/definition/api.yml @@ -0,0 +1 @@ +name: server-sent-events diff --git a/seed/php-sdk/server-sent-event-examples/.mock/definition/completions.yml b/seed/php-sdk/server-sent-event-examples/.mock/definition/completions.yml new file mode 100644 index 00000000000..09a88253331 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/.mock/definition/completions.yml @@ -0,0 +1,36 @@ +types: + StreamedCompletion: + properties: + delta: string + tokens: optional + +service: + auth: false + base-path: "" + endpoints: + stream: + method: POST + path: /stream + request: + name: StreamCompletionRequest + body: + properties: + query: string + response-stream: + type: StreamedCompletion + format: sse + terminator: "[[DONE]]" + examples: + - name: "Stream completions" + request: + query: "foo" + response: + stream: + - event: discriminant-1 + data: + delta: "foo" + tokens: 1 + - event: discriminant-2 + data: + delta: "bar" + tokens: 2 diff --git a/seed/php-sdk/server-sent-event-examples/.mock/fern.config.json b/seed/php-sdk/server-sent-event-examples/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/php-sdk/server-sent-event-examples/.mock/generators.yml b/seed/php-sdk/server-sent-event-examples/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/php-sdk/server-sent-event-examples/composer.json b/seed/php-sdk/server-sent-event-examples/composer.json new file mode 100644 index 00000000000..7f5821806d4 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/composer.json @@ -0,0 +1,40 @@ + +{ + "name": "seed/seed", + "version": "0.0.1", + "description": "Seed PHP Library", + "keywords": [ + "seed", + "api", + "sdk" + ], + "license": [], + "require": { + "php": "^8.1", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.9" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "friendsofphp/php-cs-fixer": "3.5.0", + "phpstan/phpstan": "^1.12" + }, + "autoload": { + "psr-4": { + "Seed\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "\\Seed\\Tests\\": "tests/" + } + }, + "scripts": { + "build": [ + "@php -l src", + "@php -l tests" + ], + "test": "phpunit", + "analyze": "phpstan analyze src" + } +} diff --git a/seed/php-sdk/server-sent-event-examples/phpstan.neon b/seed/php-sdk/server-sent-event-examples/phpstan.neon new file mode 100644 index 00000000000..29a11a92a19 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: max + paths: + - src + - tests \ No newline at end of file diff --git a/seed/php-sdk/server-sent-event-examples/phpunit.xml b/seed/php-sdk/server-sent-event-examples/phpunit.xml new file mode 100644 index 00000000000..54630a51163 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + \ No newline at end of file diff --git a/seed/php-sdk/server-sent-event-examples/snippet-templates.json b/seed/php-sdk/server-sent-event-examples/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/php-sdk/server-sent-event-examples/snippet.json b/seed/php-sdk/server-sent-event-examples/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/php-sdk/server-sent-event-examples/src/Completions/CompletionsClient.php b/seed/php-sdk/server-sent-event-examples/src/Completions/CompletionsClient.php new file mode 100644 index 00000000000..2b145a52d09 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Completions/CompletionsClient.php @@ -0,0 +1,58 @@ +client = $client; + } + + /** + * @param StreamCompletionRequest $request + * @param ?array{ + * baseUrl?: string, + * } $options + * @throws SeedException + * @throws SeedApiException + */ + public function stream(StreamCompletionRequest $request, ?array $options = null): void + { + try { + $response = $this->client->sendRequest( + new JsonApiRequest( + baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', + path: "stream", + method: HttpMethod::POST, + body: $request, + ), + ); + $statusCode = $response->getStatusCode(); + } catch (ClientExceptionInterface $e) { + throw new SeedException(message: $e->getMessage(), previous: $e); + } + throw new SeedApiException( + message: 'API request failed', + statusCode: $statusCode, + body: $response->getBody()->getContents(), + ); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Completions/Requests/StreamCompletionRequest.php b/seed/php-sdk/server-sent-event-examples/src/Completions/Requests/StreamCompletionRequest.php new file mode 100644 index 00000000000..2023705caff --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Completions/Requests/StreamCompletionRequest.php @@ -0,0 +1,26 @@ +query = $values['query']; + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Completions/Types/StreamedCompletion.php b/seed/php-sdk/server-sent-event-examples/src/Completions/Types/StreamedCompletion.php new file mode 100644 index 00000000000..0573b87281c --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Completions/Types/StreamedCompletion.php @@ -0,0 +1,34 @@ +delta = $values['delta']; + $this->tokens = $values['tokens'] ?? null; + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/ArrayType.php b/seed/php-sdk/server-sent-event-examples/src/Core/ArrayType.php new file mode 100644 index 00000000000..b2ed8bf12b2 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/ArrayType.php @@ -0,0 +1,16 @@ + 'valueType'] for maps, or ['valueType'] for lists + */ + public function __construct(public array $type) + { + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/BaseApiRequest.php b/seed/php-sdk/server-sent-event-examples/src/Core/BaseApiRequest.php new file mode 100644 index 00000000000..2ace034ec90 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/BaseApiRequest.php @@ -0,0 +1,22 @@ + $headers Additional headers for the request (optional) + * @param array $query Query parameters for the request (optional) + */ + public function __construct( + public readonly string $baseUrl, + public readonly string $path, + public readonly HttpMethod $method, + public readonly array $headers = [], + public readonly array $query = [], + ) { + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/Constant.php b/seed/php-sdk/server-sent-event-examples/src/Core/Constant.php new file mode 100644 index 00000000000..abbac7f6649 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/Constant.php @@ -0,0 +1,12 @@ + $headers Additional headers for the request (optional) + * @param array $query Query parameters for the request (optional) + * @param mixed|null $body The JSON request body (optional) + */ + public function __construct( + string $baseUrl, + string $path, + HttpMethod $method, + array $headers = [], + array $query = [], + public readonly mixed $body = null + ) { + parent::__construct($baseUrl, $path, $method, $headers, $query); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/JsonDecoder.php b/seed/php-sdk/server-sent-event-examples/src/Core/JsonDecoder.php new file mode 100644 index 00000000000..c7f9629e018 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/JsonDecoder.php @@ -0,0 +1,160 @@ + $type The type definition for deserialization. + * @return mixed[]|array The deserialized array. + * @throws JsonException If the decoded value is not an array. + */ + public static function decodeArray(string $json, array $type): array + { + $decoded = self::decode($json); + if (!is_array($decoded)) { + throw new JsonException("Unexpected non-array json value: " . $json); + } + return JsonDeserializer::deserializeArray($decoded, $type); + } + + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } + /** + * Decodes a JSON string and returns a mixed. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded mixed. + * @throws JsonException If the decoded value is not an mixed. + */ + public static function decodeMixed(string $json): mixed + { + return self::decode($json); + } + + /** + * Decodes a JSON string into a PHP value. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded value. + * @throws JsonException If an error occurs during JSON decoding. + */ + public static function decode(string $json): mixed + { + return json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/JsonDeserializer.php b/seed/php-sdk/server-sent-event-examples/src/Core/JsonDeserializer.php new file mode 100644 index 00000000000..b1de7d141ac --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/JsonDeserializer.php @@ -0,0 +1,202 @@ + $data The array to be deserialized. + * @param mixed[]|array $type The type definition from the annotation. + * @return mixed[]|array The deserialized array. + * @throws JsonException If deserialization fails. + */ + public static function deserializeArray(array $data, array $type): array + { + return Utils::isMapType($type) + ? self::deserializeMap($data, $type) + : self::deserializeList($data, $type); + } + + /** + * Deserializes a value based on its type definition. + * + * @param mixed $data The data to deserialize. + * @param mixed $type The type definition. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::deserializeUnion($data, $type); + } + + if (is_array($type)) { + return self::deserializeArray((array)$data, $type); + } + + if (gettype($type) != "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::deserializeSingleValue($data, $type); + } + + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + + /** + * Deserializes a single value based on its expected type. + * + * @param mixed $data The data to deserialize. + * @param string $type The expected type. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if ($type === 'date' && is_string($data)) { + return self::deserializeDate($data); + } + + if ($type === 'datetime' && is_string($data)) { + return self::deserializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && is_array($data)) { + return self::deserializeObject($data, $type); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP, and because + // floats make come through from json_decoded as integers + if ($type === 'float' && (is_numeric($data))) { + return (float) $data; + } + + if (gettype($data) === $type) { + return $data; + } + + 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. + * + * @param array $data The associative array to deserialize. + * @param array $type The type definition for the map. + * @return array The deserialized map. + * @throws JsonException If deserialization fails. + */ + private static function deserializeMap(array $data, array $type): array + { + $keyType = array_key_first($type); + $valueType = $type[$keyType]; + $result = []; + + foreach ($data as $key => $item) { + $key = Utils::castKey($key, (string)$keyType); + $result[$key] = self::deserializeValue($item, $valueType); + } + + return $result; + } + + /** + * Deserializes a list (indexed array) with a defined value type. + * + * @param array $data The list to deserialize. + * @param array $type The type definition for the list. + * @return array The deserialized list. + * @throws JsonException If deserialization fails. + */ + private static function deserializeList(array $data, array $type): array + { + $valueType = $type[0]; + return array_map(fn ($item) => self::deserializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/JsonEncoder.php b/seed/php-sdk/server-sent-event-examples/src/Core/JsonEncoder.php new file mode 100644 index 00000000000..ba5191a8068 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/JsonEncoder.php @@ -0,0 +1,20 @@ +format(Constant::DateFormat); + } + + /** + * Serializes a DateTime object into a string using the date-time format. + * + * @param DateTime $date The DateTime object to serialize. + * @return string The serialized date-time string. + */ + public static function serializeDateTime(DateTime $date): string + { + return $date->format(Constant::DateTimeFormat); + } + + /** + * Serializes an array based on type annotations (either a list or map). + * + * @param mixed[]|array $data The array to be serialized. + * @param mixed[]|array $type The type definition from the annotation. + * @return mixed[]|array The serialized array. + * @throws JsonException If serialization fails. + */ + public static function serializeArray(array $data, array $type): array + { + return Utils::isMapType($type) + ? self::serializeMap($data, $type) + : self::serializeList($data, $type); + } + + /** + * Serializes a value based on its type definition. + * + * @param mixed $data The value to serialize. + * @param mixed $type The type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::serializeUnion($data, $type); + } + + if (is_array($type)) { + return self::serializeArray((array)$data, $type); + } + + if (gettype($type) != "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::serializeSingleValue($data, $type); + } + + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + + /** + * Serializes a single value based on its type. + * + * @param mixed $data The value to serialize. + * @param string $type The expected type. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if (($type === 'date' || $type === 'datetime') && $data instanceof DateTime) { + return $type === 'date' ? self::serializeDate($data) : self::serializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && $data instanceof $type) { + return self::serializeObject($data); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP. + if ($type === 'float' && is_float($data)) { + return $data; + } + + if (gettype($data) === $type) { + return $data; + } + + 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. + * + * @param array $data The associative array to serialize. + * @param array $type The type definition for the map. + * @return array The serialized map. + * @throws JsonException If serialization fails. + */ + private static function serializeMap(array $data, array $type): array + { + $keyType = array_key_first($type); + if ($keyType === null) { + throw new JsonException("Unexpected no key in ArrayType."); + } + $valueType = $type[$keyType]; + $result = []; + + foreach ($data as $key => $item) { + $key = Utils::castKey($key, $keyType); + $result[$key] = self::serializeValue($item, $valueType); + } + + return $result; + } + + /** + * Serializes a list (indexed array) where only the value type is defined. + * + * @param array $data The list to serialize. + * @param array $type The type definition for the list. + * @return array The serialized list. + * @throws JsonException If serialization fails. + */ + private static function serializeList(array $data, array $type): array + { + $valueType = $type[0]; + return array_map(fn ($item) => self::serializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/RawClient.php b/seed/php-sdk/server-sent-event-examples/src/Core/RawClient.php new file mode 100644 index 00000000000..1c0e42bf650 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/RawClient.php @@ -0,0 +1,138 @@ + $headers + */ + private array $headers; + + /** + * @param ?array{ + * baseUrl?: string, + * client?: ClientInterface, + * headers?: array, + * } $options + */ + public function __construct( + public readonly ?array $options = null, + ) { + $this->client = $this->options['client'] ?? new Client(); + $this->headers = $this->options['headers'] ?? []; + } + + /** + * @throws ClientExceptionInterface + */ + public function sendRequest( + BaseApiRequest $request, + ): ResponseInterface { + $httpRequest = $this->buildRequest($request); + return $this->client->send($httpRequest); + } + + private function buildRequest( + BaseApiRequest $request + ): Request { + $url = $this->buildUrl($request); + $headers = $this->encodeHeaders($request); + $body = $this->encodeRequestBody($request); + return new Request( + $request->method->name, + $url, + $headers, + $body, + ); + } + + /** + * @return array + */ + private function encodeHeaders( + BaseApiRequest $request + ): array { + return match (get_class($request)) { + JsonApiRequest::class => array_merge( + ["Content-Type" => "application/json"], + $this->headers, + $request->headers + ), + default => throw new InvalidArgumentException('Unsupported request type: ' . get_class($request)), + }; + } + + private function encodeRequestBody( + BaseApiRequest $request + ): ?StreamInterface { + return match (get_class($request)) { + JsonApiRequest::class => $request->body != null ? Utils::streamFor(json_encode($request->body)) : null, + default => throw new InvalidArgumentException('Unsupported request type: '.get_class($request)), + }; + } + + private function buildUrl( + BaseApiRequest $request + ): string { + $baseUrl = $request->baseUrl; + $trimmedBaseUrl = rtrim($baseUrl, '/'); + $trimmedBasePath = ltrim($request->path, '/'); + $url = "{$trimmedBaseUrl}/{$trimmedBasePath}"; + + if (!empty($request->query)) { + $url .= '?' . $this->encodeQuery($request->query); + } + + return $url; + } + + /** + * @param array $query + */ + private function encodeQuery( + array $query + ): string { + $parts = []; + foreach ($query as $key => $value) { + if (is_array($value)) { + foreach ($value as $item) { + $parts[] = urlencode($key).'='.$this->encodeQueryValue($item); + } + } else { + $parts[] = urlencode($key).'='.$this->encodeQueryValue($value); + } + } + return implode('&', $parts); + } + + private function encodeQueryValue( + mixed $value + ): string { + if (is_string($value)) { + return urlencode($value); + } + if (is_scalar($value)) { + return urlencode((string)$value); + } + if (is_null($value)) { + return 'null'; + } + // Unreachable, but included for a best effort. + return urlencode(strval(json_encode($value))); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/SerializableType.php b/seed/php-sdk/server-sent-event-examples/src/Core/SerializableType.php new file mode 100644 index 00000000000..9121bdca01c --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/SerializableType.php @@ -0,0 +1,179 @@ +jsonSerialize(); + $encoded = JsonEncoder::encode($serializedObject); + if (!$encoded) { + throw new Exception("Could not encode type"); + } + return $encoded; + } + + /** + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. + */ + public function jsonSerialize(): array + { + $result = []; + $reflectionClass = new \ReflectionClass($this); + + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property); + if ($jsonKey == null) { + continue; + } + $value = $property->getValue($this); + + // Handle DateTime properties + $dateTypeAttr = $property->getAttributes(DateType::class)[0] ?? null; + if ($dateTypeAttr && $value instanceof DateTime) { + $dateType = $dateTypeAttr->newInstance()->type; + $value = ($dateType === DateType::TYPE_DATE) + ? JsonSerializer::serializeDate($value) + : JsonSerializer::serializeDateTime($value); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + + // Handle arrays with type annotations + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if ($arrayTypeAttr && is_array($value)) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonSerializer::serializeArray($value, $arrayType); + } + + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + + if ($value !== null) { + $result[$jsonKey] = $value; + } + } + + return $result; + } + + /** + * 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)); + } + return self::jsonDeserialize($decodedJson); + } + + /** + * Deserializes an array into an instance of the calling class. + * + * @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."); + } + + $args = []; + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property) ?? $property->getName(); + + if (array_key_exists($jsonKey, $data)) { + $value = $data[$jsonKey]; + + // Handle DateType annotation + $dateTypeAttr = $property->getAttributes(DateType::class)[0] ?? null; + if ($dateTypeAttr) { + $dateType = $dateTypeAttr->newInstance()->type; + if (!is_string($value)) { + throw new JsonException("Unexpected non-string type for date."); + } + $value = ($dateType === DateType::TYPE_DATE) + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); + } + + // Handle ArrayType annotation + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if (is_array($value) && $arrayTypeAttr) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonDeserializer::deserializeArray($value, $arrayType); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + + // 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 { + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; + } + } + // @phpstan-ignore-next-line + return new static($args); + } + + /** + * Retrieves the JSON key associated with a property. + * + * @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; + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/Union.php b/seed/php-sdk/server-sent-event-examples/src/Core/Union.php new file mode 100644 index 00000000000..1e9fe801ee7 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/Union.php @@ -0,0 +1,62 @@ +> The types allowed for this property, which can be strings, arrays, or nested Union types. + */ + public array $types; + + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) + { + $this->types = $types; + } + + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ + public function __toString(): string + { + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Core/Utils.php b/seed/php-sdk/server-sent-event-examples/src/Core/Utils.php new file mode 100644 index 00000000000..74416068d02 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Core/Utils.php @@ -0,0 +1,61 @@ + $type The type definition from the annotation. + * @return bool True if the type is a map, false if it's a list. + */ + public static function isMapType(array $type): bool + { + return count($type) === 1 && !array_is_list($type); + } + + /** + * Casts the key to the appropriate type based on the key type. + * + * @param mixed $key The key to be cast. + * @param string $keyType The type to cast the key to ('string', 'integer', 'float'). + * @return mixed The casted key. + * @throws JsonException + */ + public static function castKey(mixed $key, string $keyType): mixed + { + if (!is_scalar($key)) { + throw new JsonException("Key must be a scalar type."); + } + return match ($keyType) { + 'integer' => (int)$key, + 'float' => (float)$key, + 'string' => (string)$key, + 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/server-sent-event-examples/src/Exceptions/SeedApiException.php b/seed/php-sdk/server-sent-event-examples/src/Exceptions/SeedApiException.php new file mode 100644 index 00000000000..41a85392b70 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Exceptions/SeedApiException.php @@ -0,0 +1,53 @@ +body = $body; + parent::__construct($message, $statusCode, $previous); + } + + /** + * Returns the body of the response that triggered the exception. + * + * @return mixed + */ + public function getBody(): mixed + { + return $this->body; + } + + /** + * @return string + */ + public function __toString(): string + { + if (empty($this->body)) { + return "$this->message; Status Code: $this->code\n"; + } + return "$this->message; Status Code: $this->code; Body: " . $this->body . "\n"; + } +} diff --git a/seed/php-sdk/server-sent-event-examples/src/Exceptions/SeedException.php b/seed/php-sdk/server-sent-event-examples/src/Exceptions/SeedException.php new file mode 100644 index 00000000000..45703527673 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/src/Exceptions/SeedException.php @@ -0,0 +1,12 @@ +, + * } $options + */ + private ?array $options; + + /** + * @var RawClient $client + */ + private RawClient $client; + + /** + * @param ?array{ + * baseUrl?: string, + * client?: ClientInterface, + * headers?: array, + * } $options + */ + public function __construct( + ?array $options = null, + ) { + $defaultHeaders = [ + 'X-Fern-Language' => 'PHP', + 'X-Fern-SDK-Name' => 'Seed', + 'X-Fern-SDK-Version' => '0.0.1', + ]; + + $this->options = $options ?? []; + $this->options['headers'] = array_merge( + $defaultHeaders, + $this->options['headers'] ?? [], + ); + + $this->client = new RawClient( + options: $this->options, + ); + + $this->completions = new CompletionsClient($this->client); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/DateArrayTypeTest.php new file mode 100644 index 00000000000..8d93afc9e44 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/DateArrayTypeTest.php @@ -0,0 +1,55 @@ +dates = $values['dates']; + } +} + +class DateArrayTypeTest extends TestCase +{ + public function testDateTimeTypesInArrays(): void + { + $data = [ + 'dates' => ['2023-01-01', '2023-02-01', '2023-03-01'] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = DateArrayType::fromJson($json); + + $this->assertInstanceOf(DateTime::class, $object->dates[0], 'dates[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dates[0]->format('Y-m-d'), 'dates[0] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[1], 'dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-02-01', $object->dates[1]->format('Y-m-d'), 'dates[1] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[2], 'dates[2] should be a DateTime instance.'); + $this->assertEquals('2023-03-01', $object->dates[2]->format('Y-m-d'), 'dates[2] should have the correct date.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for dates array.'); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/EmptyArraysTest.php new file mode 100644 index 00000000000..b44f3d093e6 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/EmptyArraysTest.php @@ -0,0 +1,73 @@ + $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( + array $values, + ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; + } +} + +class EmptyArraysTest extends TestCase +{ + public function testEmptyArrays(): void + { + $data = [ + 'empty_string_array' => [], + 'empty_map_array' => [], + 'empty_dates_array' => [] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = EmptyArraysType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for EmptyArraysType.'); + + // Check that arrays are empty + $this->assertEmpty($object->emptyStringArray, 'empty_string_array should be empty.'); + $this->assertEmpty($object->emptyMapArray, 'empty_map_array should be empty.'); + $this->assertEmpty($object->emptyDatesArray, 'empty_dates_array should be empty.'); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/EnumTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/EnumTest.php new file mode 100644 index 00000000000..ef5b8484dfd --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/EnumTest.php @@ -0,0 +1,76 @@ +value; + } +} + +class ShapeType extends SerializableType +{ + /** + * @var Shape $shape + */ + #[JsonProperty('shape')] + public Shape $shape; + + /** + * @var Shape[] $shapes + */ + #[ArrayType([Shape::class])] + #[JsonProperty('shapes')] + public array $shapes; + + /** + * @param Shape $shape + * @param Shape[] $shapes + */ + public function __construct( + Shape $shape, + array $shapes, + ) { + $this->shape = $shape; + $this->shapes = $shapes; + } +} + +class EnumTest extends TestCase +{ + public function testShapeEnumSerialization(): void + { + $object = new ShapeType( + Shape::Circle, + [Shape::Square, Shape::Circle, Shape::Triangle] + ); + + $expectedJson = json_encode([ + 'shape' => 'CIRCLE', + 'shapes' => ['SQUARE', 'CIRCLE', 'TRIANGLE'] + ], JSON_THROW_ON_ERROR); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString( + $expectedJson, + $serializedJson, + 'Serialized JSON does not match expected JSON for shape and shapes properties.' + ); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/InvalidTypesTest.php new file mode 100644 index 00000000000..67bfd235b2f --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/InvalidTypesTest.php @@ -0,0 +1,45 @@ +integerProperty = $values['integerProperty']; + } +} + +class InvalidTypesTest extends TestCase +{ + public function testInvalidTypesThrowExceptions(): void + { + // Create test data with invalid type for integer_property (string instead of int) + $data = [ + 'integer_property' => 'not_an_integer' + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $this->expectException(\TypeError::class); + + // Attempt to deserialize invalid data + InvalidType::fromJson($json); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/MixedDateArrayTypeTest.php new file mode 100644 index 00000000000..3bf18aec25b --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -0,0 +1,60 @@ + $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->mixedDates = $values['mixedDates']; + } +} + +class MixedDateArrayTypeTest extends TestCase +{ + public function testDateTimeTypesInUnionArrays(): void + { + $data = [ + 'mixed_dates' => [ + 1 => '2023-01-01T12:00:00+00:00', + 2 => null, + 3 => 'Some String' + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = MixedDateArrayType::fromJson($json); + + $this->assertInstanceOf(DateTime::class, $object->mixedDates[1], 'mixed_dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:00:00', $object->mixedDates[1]->format('Y-m-d H:i:s'), 'mixed_dates[1] should have the correct datetime.'); + + $this->assertNull($object->mixedDates[2], 'mixed_dates[2] should be null.'); + + $this->assertEquals('Some String', $object->mixedDates[3], 'mixed_dates[3] should be "Some String".'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for mixed_dates.'); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/NestedUnionArrayTypeTest.php new file mode 100644 index 00000000000..4667ecafcb9 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -0,0 +1,99 @@ +nestedProperty = $values['nestedProperty']; + } +} + +class NestedUnionArrayType extends SerializableType +{ + /** + * @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( + array $values, + ) { + $this->nestedArray = $values['nestedArray']; + } +} + +class NestedUnionArrayTypeTest extends TestCase +{ + public function testNestedUnionTypesInArrays(): void + { + $data = [ + 'nested_array' => [ + 1 => [ + 1 => ['nested_property' => 'Nested One'], + 2 => null, + 4 => '2023-01-02' + ], + 2 => [ + 5 => ['nested_property' => 'Nested Two'], + 7 => '2023-02-02' + ] + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = NestedUnionArrayType::fromJson($json); + + // Level 1 + $this->assertInstanceOf(TestNestedType::class, $object->nestedArray[1][1], 'nested_array[1][1] should be an instance of TestNestedType.'); + $this->assertEquals('Nested One', $object->nestedArray[1][1]->nestedProperty, 'nested_array[1][1]->nestedProperty should match the original data.'); + + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + + // ensure dates are set with the default time + $this->assertInstanceOf(DateTime::class, $object->nestedArray[1][4], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-01-02T00:00:00+00:00', $object->nestedArray[1][4]->format(Constant::DateTimeFormat), 'nested_array[1][4] should have the correct datetime.'); + + // Level 2 + $this->assertInstanceOf(TestNestedType::class, $object->nestedArray[2][5], 'nested_array[2][5] should be an instance of TestNestedType.'); + $this->assertEquals('Nested Two', $object->nestedArray[2][5]->nestedProperty, 'nested_array[2][5]->nestedProperty should match the original data.'); + + $this->assertInstanceOf(DateTime::class, $object->nestedArray[2][7], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-02-02', $object->nestedArray[2][7]->format('Y-m-d'), 'nested_array[1][4] should have the correct date.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for nested_array.'); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/NullPropertyTypeTest.php new file mode 100644 index 00000000000..134296f56e3 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/NullPropertyTypeTest.php @@ -0,0 +1,50 @@ +nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; + } +} + +class NullPropertyTypeTest extends TestCase +{ + public function testNullPropertiesAreOmitted(): void + { + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); + + $serializedObject = $object->jsonSerialize(); + + $this->assertArrayHasKey('non_null_property', $serializedObject, 'non_null_property should be present in the serialized JSON.'); + $this->assertArrayNotHasKey('null_property', $serializedObject, 'null_property should be omitted from the serialized JSON.'); + + $this->assertEquals('Test String', $serializedObject['non_null_property'], 'non_null_property should have the correct value.'); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/NullableArrayTypeTest.php new file mode 100644 index 00000000000..bf6345e5c6f --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/NullableArrayTypeTest.php @@ -0,0 +1,50 @@ + $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->nullableStringArray = $values['nullableStringArray']; + } +} + +class NullableArrayTypeTest extends TestCase +{ + public function testNullableTypesInArrays(): void + { + $data = [ + 'nullable_string_array' => ['one', null, 'three'] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = NullableArrayType::fromJson($json); + + $this->assertEquals(['one', null, 'three'], $object->nullableStringArray, 'nullable_string_array should match the original data.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for nullable_string_array.'); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/RawClientTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/RawClientTest.php new file mode 100644 index 00000000000..e01ae63b41a --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/RawClientTest.php @@ -0,0 +1,101 @@ +mockHandler = new MockHandler(); + $handlerStack = HandlerStack::create($this->mockHandler); + $client = new Client(['handler' => $handlerStack]); + $this->rawClient = new RawClient(['client' => $client]); + } + + public function testHeaders(): void + { + $this->mockHandler->append(new Response(200)); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::GET, + ['X-Custom-Header' => 'TestValue'] + ); + + $this->sendRequest($request); + + $lastRequest = $this->mockHandler->getLastRequest(); + assert($lastRequest instanceof RequestInterface); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals('TestValue', $lastRequest->getHeaderLine('X-Custom-Header')); + } + + public function testQueryParameters(): void + { + $this->mockHandler->append(new Response(200)); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::GET, + [], + ['param1' => 'value1', 'param2' => ['a', 'b'], 'param3' => 'true'] + ); + + $this->sendRequest($request); + + $lastRequest = $this->mockHandler->getLastRequest(); + assert($lastRequest instanceof RequestInterface); + $this->assertEquals( + 'https://api.example.com/test?param1=value1¶m2=a¶m2=b¶m3=true', + (string)$lastRequest->getUri() + ); + } + + public function testJsonBody(): void + { + $this->mockHandler->append(new Response(200)); + + $body = ['key' => 'value']; + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::POST, + [], + [], + $body + ); + + $this->sendRequest($request); + + $lastRequest = $this->mockHandler->getLastRequest(); + assert($lastRequest instanceof RequestInterface); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals(json_encode($body), (string)$lastRequest->getBody()); + } + + private function sendRequest(BaseApiRequest $request): void + { + try { + $this->rawClient->sendRequest($request); + } catch (\Throwable $e) { + $this->fail('An exception was thrown: ' . $e->getMessage()); + } + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/ScalarTypesTest.php new file mode 100644 index 00000000000..899e949836c --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/ScalarTypesTest.php @@ -0,0 +1,121 @@ + $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var array $floatArray + */ + #[ArrayType(['float'])] + #[JsonProperty('float_array')] + public array $floatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * otherFloatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * floatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ + public function __construct( + array $values, + ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->otherFloatProperty = $values['otherFloatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->floatArray = $values['floatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; + } +} + +class ScalarTypesTest extends TestCase +{ + public function testAllScalarTypesIncludingFloat(): void + { + // Create test data + $data = [ + 'integer_property' => 42, + 'float_property' => 3.14159, + 'other_float_property' => 3, + 'boolean_property' => true, + 'string_property' => 'Hello, World!', + 'int_float_array' => [1, 2.5, 3, 4.75], + 'float_array' => [1, 2, 3, 4] // ensure we handle "integer-looking" floats + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = ScalarTypesTestType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for ScalarTypesTest.'); + + // Check scalar properties + $this->assertEquals(42, $object->integerProperty, 'integer_property should be 42.'); + $this->assertEquals(3.14159, $object->floatProperty, 'float_property should be 3.14159.'); + $this->assertTrue($object->booleanProperty, 'boolean_property should be true.'); + $this->assertEquals('Hello, World!', $object->stringProperty, 'string_property should be "Hello, World!".'); + $this->assertNull($object->nullableBooleanProperty, 'nullable_boolean_property should be null.'); + + // Check int_float_array + $this->assertEquals([1, 2.5, 3, 4.75], $object->intFloatArray, 'int_float_array should match the original data.'); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/TestTypeTest.php new file mode 100644 index 00000000000..8e7ca1b825c --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/TestTypeTest.php @@ -0,0 +1,201 @@ +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( + 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; + } +} + +class TestTypeTest extends TestCase +{ + /** + * Test serialization and deserialization of all types in TestType + */ + 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', + 'datetime_property' => '2023-01-01T12:34:56+00:00', + 'string_array' => ['one', 'two', 'three'], + 'map_property' => ['key1' => 1, 'key2' => 2], + 'object_array' => [ + 1 => ['nested_property' => '2021-07-20'], + 2 => null, // Testing nullable objects in array + ], + 'nested_array' => [ + 1 => [1 => 'value1', 2 => null], // Testing nullable strings in nested array + 2 => [3 => 'value3', 4 => 'value4'] + ], + 'dates_array' => ['2023-01-01', null, '2023-03-01'] // Testing nullable dates in array + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = TestType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'The serialized JSON does not match the original JSON.'); + + // Check that nullable property is null and not included in JSON + $this->assertNull($object->nullableProperty, 'Nullable property should be null.'); + // @phpstan-ignore-next-line + $this->assertFalse(array_key_exists('nullable_property', json_decode($serializedJson, true)), 'Nullable property should be omitted from JSON.'); + + // Check date properties + $this->assertInstanceOf(DateTime::class, $object->dateProperty, 'date_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dateProperty->format('Y-m-d'), 'date_property should have the correct date.'); + + $this->assertInstanceOf(DateTime::class, $object->datetimeProperty, 'datetime_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:34:56', $object->datetimeProperty->format('Y-m-d H:i:s'), 'datetime_property should have the correct datetime.'); + + // Check scalar arrays + $this->assertEquals(['one', 'two', 'three'], $object->stringArray, 'string_array should match the original data.'); + $this->assertEquals(['key1' => 1, 'key2' => 2], $object->mapProperty, 'map_property should match the original data.'); + + // Check object array with nullable elements + $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); + $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 + $this->assertEquals('value1', $object->nestedArray[1][1], 'nested_array[1][1] should match the original data.'); + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + $this->assertEquals('value3', $object->nestedArray[2][3], 'nested_array[2][3] should match the original data.'); + $this->assertEquals('value4', $object->nestedArray[2][4], 'nested_array[2][4] should match the original data.'); + + // Check dates array with nullable DateTime objects + $this->assertInstanceOf(DateTime::class, $object->datesArray[0], 'dates_array[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->datesArray[0]->format('Y-m-d'), 'dates_array[0] should have the correct date.'); + $this->assertNull($object->datesArray[1], 'dates_array[1] should be null.'); + $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.'); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/UnionArrayTypeTest.php new file mode 100644 index 00000000000..8d0998f4b7e --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/UnionArrayTypeTest.php @@ -0,0 +1,56 @@ + $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->mixedArray = $values['mixedArray']; + } +} + +class UnionArrayTypeTest extends TestCase +{ + public function testUnionTypesInArrays(): void + { + $data = [ + 'mixed_array' => [ + 1 => 'one', + 2 => 2, + 3 => null + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = UnionArrayType::fromJson($json); + + $this->assertEquals('one', $object->mixedArray[1], 'mixed_array[1] should be "one".'); + $this->assertEquals(2, $object->mixedArray[2], 'mixed_array[2] should be 2.'); + $this->assertNull($object->mixedArray[3], 'mixed_array[3] should be null.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for mixed_array.'); + } +} diff --git a/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/server-sent-event-examples/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/server-sent-events/.github/workflows/ci.yml b/seed/php-sdk/server-sent-events/.github/workflows/ci.yml new file mode 100644 index 00000000000..258bf33a19f --- /dev/null +++ b/seed/php-sdk/server-sent-events/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + + - name: Install tools + run: | + composer install + + - name: Build + run: | + composer build + + - name: Analyze + run: | + composer analyze + + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + + - name: Install tools + run: | + composer install + + - name: Run Tests + run: | + composer test \ No newline at end of file diff --git a/seed/php-sdk/server-sent-events/.gitignore b/seed/php-sdk/server-sent-events/.gitignore new file mode 100644 index 00000000000..f38efc46ade --- /dev/null +++ b/seed/php-sdk/server-sent-events/.gitignore @@ -0,0 +1,4 @@ +.php-cs-fixer.cache +.phpunit.result.cache +composer.lock +vendor/ \ No newline at end of file diff --git a/seed/php-sdk/server-sent-events/.mock/definition/api.yml b/seed/php-sdk/server-sent-events/.mock/definition/api.yml new file mode 100644 index 00000000000..80e84c41785 --- /dev/null +++ b/seed/php-sdk/server-sent-events/.mock/definition/api.yml @@ -0,0 +1 @@ +name: server-sent-events diff --git a/seed/php-sdk/server-sent-events/.mock/definition/completions.yml b/seed/php-sdk/server-sent-events/.mock/definition/completions.yml new file mode 100644 index 00000000000..d1748fad19e --- /dev/null +++ b/seed/php-sdk/server-sent-events/.mock/definition/completions.yml @@ -0,0 +1,22 @@ +types: + StreamedCompletion: + properties: + delta: string + tokens: optional + +service: + auth: false + base-path: "" + endpoints: + stream: + method: POST + path: /stream + request: + name: StreamCompletionRequest + body: + properties: + query: string + response-stream: + type: StreamedCompletion + format: sse + terminator: "[[DONE]]" diff --git a/seed/php-sdk/server-sent-events/.mock/fern.config.json b/seed/php-sdk/server-sent-events/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/php-sdk/server-sent-events/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/php-sdk/server-sent-events/.mock/generators.yml b/seed/php-sdk/server-sent-events/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/php-sdk/server-sent-events/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/php-sdk/server-sent-events/composer.json b/seed/php-sdk/server-sent-events/composer.json new file mode 100644 index 00000000000..7f5821806d4 --- /dev/null +++ b/seed/php-sdk/server-sent-events/composer.json @@ -0,0 +1,40 @@ + +{ + "name": "seed/seed", + "version": "0.0.1", + "description": "Seed PHP Library", + "keywords": [ + "seed", + "api", + "sdk" + ], + "license": [], + "require": { + "php": "^8.1", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.9" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "friendsofphp/php-cs-fixer": "3.5.0", + "phpstan/phpstan": "^1.12" + }, + "autoload": { + "psr-4": { + "Seed\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "\\Seed\\Tests\\": "tests/" + } + }, + "scripts": { + "build": [ + "@php -l src", + "@php -l tests" + ], + "test": "phpunit", + "analyze": "phpstan analyze src" + } +} diff --git a/seed/php-sdk/server-sent-events/phpstan.neon b/seed/php-sdk/server-sent-events/phpstan.neon new file mode 100644 index 00000000000..29a11a92a19 --- /dev/null +++ b/seed/php-sdk/server-sent-events/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: max + paths: + - src + - tests \ No newline at end of file diff --git a/seed/php-sdk/server-sent-events/phpunit.xml b/seed/php-sdk/server-sent-events/phpunit.xml new file mode 100644 index 00000000000..54630a51163 --- /dev/null +++ b/seed/php-sdk/server-sent-events/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + \ No newline at end of file diff --git a/seed/php-sdk/server-sent-events/snippet-templates.json b/seed/php-sdk/server-sent-events/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/php-sdk/server-sent-events/snippet.json b/seed/php-sdk/server-sent-events/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/php-sdk/server-sent-events/src/Completions/CompletionsClient.php b/seed/php-sdk/server-sent-events/src/Completions/CompletionsClient.php new file mode 100644 index 00000000000..2b145a52d09 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Completions/CompletionsClient.php @@ -0,0 +1,58 @@ +client = $client; + } + + /** + * @param StreamCompletionRequest $request + * @param ?array{ + * baseUrl?: string, + * } $options + * @throws SeedException + * @throws SeedApiException + */ + public function stream(StreamCompletionRequest $request, ?array $options = null): void + { + try { + $response = $this->client->sendRequest( + new JsonApiRequest( + baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', + path: "stream", + method: HttpMethod::POST, + body: $request, + ), + ); + $statusCode = $response->getStatusCode(); + } catch (ClientExceptionInterface $e) { + throw new SeedException(message: $e->getMessage(), previous: $e); + } + throw new SeedApiException( + message: 'API request failed', + statusCode: $statusCode, + body: $response->getBody()->getContents(), + ); + } +} diff --git a/seed/php-sdk/server-sent-events/src/Completions/Requests/StreamCompletionRequest.php b/seed/php-sdk/server-sent-events/src/Completions/Requests/StreamCompletionRequest.php new file mode 100644 index 00000000000..2023705caff --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Completions/Requests/StreamCompletionRequest.php @@ -0,0 +1,26 @@ +query = $values['query']; + } +} diff --git a/seed/php-sdk/server-sent-events/src/Completions/Types/StreamedCompletion.php b/seed/php-sdk/server-sent-events/src/Completions/Types/StreamedCompletion.php new file mode 100644 index 00000000000..0573b87281c --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Completions/Types/StreamedCompletion.php @@ -0,0 +1,34 @@ +delta = $values['delta']; + $this->tokens = $values['tokens'] ?? null; + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/ArrayType.php b/seed/php-sdk/server-sent-events/src/Core/ArrayType.php new file mode 100644 index 00000000000..b2ed8bf12b2 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/ArrayType.php @@ -0,0 +1,16 @@ + 'valueType'] for maps, or ['valueType'] for lists + */ + public function __construct(public array $type) + { + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/BaseApiRequest.php b/seed/php-sdk/server-sent-events/src/Core/BaseApiRequest.php new file mode 100644 index 00000000000..2ace034ec90 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/BaseApiRequest.php @@ -0,0 +1,22 @@ + $headers Additional headers for the request (optional) + * @param array $query Query parameters for the request (optional) + */ + public function __construct( + public readonly string $baseUrl, + public readonly string $path, + public readonly HttpMethod $method, + public readonly array $headers = [], + public readonly array $query = [], + ) { + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/Constant.php b/seed/php-sdk/server-sent-events/src/Core/Constant.php new file mode 100644 index 00000000000..abbac7f6649 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/Constant.php @@ -0,0 +1,12 @@ + $headers Additional headers for the request (optional) + * @param array $query Query parameters for the request (optional) + * @param mixed|null $body The JSON request body (optional) + */ + public function __construct( + string $baseUrl, + string $path, + HttpMethod $method, + array $headers = [], + array $query = [], + public readonly mixed $body = null + ) { + parent::__construct($baseUrl, $path, $method, $headers, $query); + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/JsonDecoder.php b/seed/php-sdk/server-sent-events/src/Core/JsonDecoder.php new file mode 100644 index 00000000000..c7f9629e018 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/JsonDecoder.php @@ -0,0 +1,160 @@ + $type The type definition for deserialization. + * @return mixed[]|array The deserialized array. + * @throws JsonException If the decoded value is not an array. + */ + public static function decodeArray(string $json, array $type): array + { + $decoded = self::decode($json); + if (!is_array($decoded)) { + throw new JsonException("Unexpected non-array json value: " . $json); + } + return JsonDeserializer::deserializeArray($decoded, $type); + } + + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } + /** + * Decodes a JSON string and returns a mixed. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded mixed. + * @throws JsonException If the decoded value is not an mixed. + */ + public static function decodeMixed(string $json): mixed + { + return self::decode($json); + } + + /** + * Decodes a JSON string into a PHP value. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded value. + * @throws JsonException If an error occurs during JSON decoding. + */ + public static function decode(string $json): mixed + { + return json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR); + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/JsonDeserializer.php b/seed/php-sdk/server-sent-events/src/Core/JsonDeserializer.php new file mode 100644 index 00000000000..b1de7d141ac --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/JsonDeserializer.php @@ -0,0 +1,202 @@ + $data The array to be deserialized. + * @param mixed[]|array $type The type definition from the annotation. + * @return mixed[]|array The deserialized array. + * @throws JsonException If deserialization fails. + */ + public static function deserializeArray(array $data, array $type): array + { + return Utils::isMapType($type) + ? self::deserializeMap($data, $type) + : self::deserializeList($data, $type); + } + + /** + * Deserializes a value based on its type definition. + * + * @param mixed $data The data to deserialize. + * @param mixed $type The type definition. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::deserializeUnion($data, $type); + } + + if (is_array($type)) { + return self::deserializeArray((array)$data, $type); + } + + if (gettype($type) != "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::deserializeSingleValue($data, $type); + } + + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + + /** + * Deserializes a single value based on its expected type. + * + * @param mixed $data The data to deserialize. + * @param string $type The expected type. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if ($type === 'date' && is_string($data)) { + return self::deserializeDate($data); + } + + if ($type === 'datetime' && is_string($data)) { + return self::deserializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && is_array($data)) { + return self::deserializeObject($data, $type); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP, and because + // floats make come through from json_decoded as integers + if ($type === 'float' && (is_numeric($data))) { + return (float) $data; + } + + if (gettype($data) === $type) { + return $data; + } + + 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. + * + * @param array $data The associative array to deserialize. + * @param array $type The type definition for the map. + * @return array The deserialized map. + * @throws JsonException If deserialization fails. + */ + private static function deserializeMap(array $data, array $type): array + { + $keyType = array_key_first($type); + $valueType = $type[$keyType]; + $result = []; + + foreach ($data as $key => $item) { + $key = Utils::castKey($key, (string)$keyType); + $result[$key] = self::deserializeValue($item, $valueType); + } + + return $result; + } + + /** + * Deserializes a list (indexed array) with a defined value type. + * + * @param array $data The list to deserialize. + * @param array $type The type definition for the list. + * @return array The deserialized list. + * @throws JsonException If deserialization fails. + */ + private static function deserializeList(array $data, array $type): array + { + $valueType = $type[0]; + return array_map(fn ($item) => self::deserializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/JsonEncoder.php b/seed/php-sdk/server-sent-events/src/Core/JsonEncoder.php new file mode 100644 index 00000000000..ba5191a8068 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/JsonEncoder.php @@ -0,0 +1,20 @@ +format(Constant::DateFormat); + } + + /** + * Serializes a DateTime object into a string using the date-time format. + * + * @param DateTime $date The DateTime object to serialize. + * @return string The serialized date-time string. + */ + public static function serializeDateTime(DateTime $date): string + { + return $date->format(Constant::DateTimeFormat); + } + + /** + * Serializes an array based on type annotations (either a list or map). + * + * @param mixed[]|array $data The array to be serialized. + * @param mixed[]|array $type The type definition from the annotation. + * @return mixed[]|array The serialized array. + * @throws JsonException If serialization fails. + */ + public static function serializeArray(array $data, array $type): array + { + return Utils::isMapType($type) + ? self::serializeMap($data, $type) + : self::serializeList($data, $type); + } + + /** + * Serializes a value based on its type definition. + * + * @param mixed $data The value to serialize. + * @param mixed $type The type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::serializeUnion($data, $type); + } + + if (is_array($type)) { + return self::serializeArray((array)$data, $type); + } + + if (gettype($type) != "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::serializeSingleValue($data, $type); + } + + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + + /** + * Serializes a single value based on its type. + * + * @param mixed $data The value to serialize. + * @param string $type The expected type. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if (($type === 'date' || $type === 'datetime') && $data instanceof DateTime) { + return $type === 'date' ? self::serializeDate($data) : self::serializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && $data instanceof $type) { + return self::serializeObject($data); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP. + if ($type === 'float' && is_float($data)) { + return $data; + } + + if (gettype($data) === $type) { + return $data; + } + + 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. + * + * @param array $data The associative array to serialize. + * @param array $type The type definition for the map. + * @return array The serialized map. + * @throws JsonException If serialization fails. + */ + private static function serializeMap(array $data, array $type): array + { + $keyType = array_key_first($type); + if ($keyType === null) { + throw new JsonException("Unexpected no key in ArrayType."); + } + $valueType = $type[$keyType]; + $result = []; + + foreach ($data as $key => $item) { + $key = Utils::castKey($key, $keyType); + $result[$key] = self::serializeValue($item, $valueType); + } + + return $result; + } + + /** + * Serializes a list (indexed array) where only the value type is defined. + * + * @param array $data The list to serialize. + * @param array $type The type definition for the list. + * @return array The serialized list. + * @throws JsonException If serialization fails. + */ + private static function serializeList(array $data, array $type): array + { + $valueType = $type[0]; + return array_map(fn ($item) => self::serializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/RawClient.php b/seed/php-sdk/server-sent-events/src/Core/RawClient.php new file mode 100644 index 00000000000..1c0e42bf650 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/RawClient.php @@ -0,0 +1,138 @@ + $headers + */ + private array $headers; + + /** + * @param ?array{ + * baseUrl?: string, + * client?: ClientInterface, + * headers?: array, + * } $options + */ + public function __construct( + public readonly ?array $options = null, + ) { + $this->client = $this->options['client'] ?? new Client(); + $this->headers = $this->options['headers'] ?? []; + } + + /** + * @throws ClientExceptionInterface + */ + public function sendRequest( + BaseApiRequest $request, + ): ResponseInterface { + $httpRequest = $this->buildRequest($request); + return $this->client->send($httpRequest); + } + + private function buildRequest( + BaseApiRequest $request + ): Request { + $url = $this->buildUrl($request); + $headers = $this->encodeHeaders($request); + $body = $this->encodeRequestBody($request); + return new Request( + $request->method->name, + $url, + $headers, + $body, + ); + } + + /** + * @return array + */ + private function encodeHeaders( + BaseApiRequest $request + ): array { + return match (get_class($request)) { + JsonApiRequest::class => array_merge( + ["Content-Type" => "application/json"], + $this->headers, + $request->headers + ), + default => throw new InvalidArgumentException('Unsupported request type: ' . get_class($request)), + }; + } + + private function encodeRequestBody( + BaseApiRequest $request + ): ?StreamInterface { + return match (get_class($request)) { + JsonApiRequest::class => $request->body != null ? Utils::streamFor(json_encode($request->body)) : null, + default => throw new InvalidArgumentException('Unsupported request type: '.get_class($request)), + }; + } + + private function buildUrl( + BaseApiRequest $request + ): string { + $baseUrl = $request->baseUrl; + $trimmedBaseUrl = rtrim($baseUrl, '/'); + $trimmedBasePath = ltrim($request->path, '/'); + $url = "{$trimmedBaseUrl}/{$trimmedBasePath}"; + + if (!empty($request->query)) { + $url .= '?' . $this->encodeQuery($request->query); + } + + return $url; + } + + /** + * @param array $query + */ + private function encodeQuery( + array $query + ): string { + $parts = []; + foreach ($query as $key => $value) { + if (is_array($value)) { + foreach ($value as $item) { + $parts[] = urlencode($key).'='.$this->encodeQueryValue($item); + } + } else { + $parts[] = urlencode($key).'='.$this->encodeQueryValue($value); + } + } + return implode('&', $parts); + } + + private function encodeQueryValue( + mixed $value + ): string { + if (is_string($value)) { + return urlencode($value); + } + if (is_scalar($value)) { + return urlencode((string)$value); + } + if (is_null($value)) { + return 'null'; + } + // Unreachable, but included for a best effort. + return urlencode(strval(json_encode($value))); + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/SerializableType.php b/seed/php-sdk/server-sent-events/src/Core/SerializableType.php new file mode 100644 index 00000000000..9121bdca01c --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/SerializableType.php @@ -0,0 +1,179 @@ +jsonSerialize(); + $encoded = JsonEncoder::encode($serializedObject); + if (!$encoded) { + throw new Exception("Could not encode type"); + } + return $encoded; + } + + /** + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. + */ + public function jsonSerialize(): array + { + $result = []; + $reflectionClass = new \ReflectionClass($this); + + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property); + if ($jsonKey == null) { + continue; + } + $value = $property->getValue($this); + + // Handle DateTime properties + $dateTypeAttr = $property->getAttributes(DateType::class)[0] ?? null; + if ($dateTypeAttr && $value instanceof DateTime) { + $dateType = $dateTypeAttr->newInstance()->type; + $value = ($dateType === DateType::TYPE_DATE) + ? JsonSerializer::serializeDate($value) + : JsonSerializer::serializeDateTime($value); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + + // Handle arrays with type annotations + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if ($arrayTypeAttr && is_array($value)) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonSerializer::serializeArray($value, $arrayType); + } + + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + + if ($value !== null) { + $result[$jsonKey] = $value; + } + } + + return $result; + } + + /** + * 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)); + } + return self::jsonDeserialize($decodedJson); + } + + /** + * Deserializes an array into an instance of the calling class. + * + * @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."); + } + + $args = []; + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property) ?? $property->getName(); + + if (array_key_exists($jsonKey, $data)) { + $value = $data[$jsonKey]; + + // Handle DateType annotation + $dateTypeAttr = $property->getAttributes(DateType::class)[0] ?? null; + if ($dateTypeAttr) { + $dateType = $dateTypeAttr->newInstance()->type; + if (!is_string($value)) { + throw new JsonException("Unexpected non-string type for date."); + } + $value = ($dateType === DateType::TYPE_DATE) + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); + } + + // Handle ArrayType annotation + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if (is_array($value) && $arrayTypeAttr) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonDeserializer::deserializeArray($value, $arrayType); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + + // 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 { + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; + } + } + // @phpstan-ignore-next-line + return new static($args); + } + + /** + * Retrieves the JSON key associated with a property. + * + * @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; + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/Union.php b/seed/php-sdk/server-sent-events/src/Core/Union.php new file mode 100644 index 00000000000..1e9fe801ee7 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/Union.php @@ -0,0 +1,62 @@ +> The types allowed for this property, which can be strings, arrays, or nested Union types. + */ + public array $types; + + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) + { + $this->types = $types; + } + + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ + public function __toString(): string + { + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); + } +} diff --git a/seed/php-sdk/server-sent-events/src/Core/Utils.php b/seed/php-sdk/server-sent-events/src/Core/Utils.php new file mode 100644 index 00000000000..74416068d02 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Core/Utils.php @@ -0,0 +1,61 @@ + $type The type definition from the annotation. + * @return bool True if the type is a map, false if it's a list. + */ + public static function isMapType(array $type): bool + { + return count($type) === 1 && !array_is_list($type); + } + + /** + * Casts the key to the appropriate type based on the key type. + * + * @param mixed $key The key to be cast. + * @param string $keyType The type to cast the key to ('string', 'integer', 'float'). + * @return mixed The casted key. + * @throws JsonException + */ + public static function castKey(mixed $key, string $keyType): mixed + { + if (!is_scalar($key)) { + throw new JsonException("Key must be a scalar type."); + } + return match ($keyType) { + 'integer' => (int)$key, + 'float' => (float)$key, + 'string' => (string)$key, + 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/server-sent-events/src/Exceptions/SeedApiException.php b/seed/php-sdk/server-sent-events/src/Exceptions/SeedApiException.php new file mode 100644 index 00000000000..41a85392b70 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Exceptions/SeedApiException.php @@ -0,0 +1,53 @@ +body = $body; + parent::__construct($message, $statusCode, $previous); + } + + /** + * Returns the body of the response that triggered the exception. + * + * @return mixed + */ + public function getBody(): mixed + { + return $this->body; + } + + /** + * @return string + */ + public function __toString(): string + { + if (empty($this->body)) { + return "$this->message; Status Code: $this->code\n"; + } + return "$this->message; Status Code: $this->code; Body: " . $this->body . "\n"; + } +} diff --git a/seed/php-sdk/server-sent-events/src/Exceptions/SeedException.php b/seed/php-sdk/server-sent-events/src/Exceptions/SeedException.php new file mode 100644 index 00000000000..45703527673 --- /dev/null +++ b/seed/php-sdk/server-sent-events/src/Exceptions/SeedException.php @@ -0,0 +1,12 @@ +, + * } $options + */ + private ?array $options; + + /** + * @var RawClient $client + */ + private RawClient $client; + + /** + * @param ?array{ + * baseUrl?: string, + * client?: ClientInterface, + * headers?: array, + * } $options + */ + public function __construct( + ?array $options = null, + ) { + $defaultHeaders = [ + 'X-Fern-Language' => 'PHP', + 'X-Fern-SDK-Name' => 'Seed', + 'X-Fern-SDK-Version' => '0.0.1', + ]; + + $this->options = $options ?? []; + $this->options['headers'] = array_merge( + $defaultHeaders, + $this->options['headers'] ?? [], + ); + + $this->client = new RawClient( + options: $this->options, + ); + + $this->completions = new CompletionsClient($this->client); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/DateArrayTypeTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/DateArrayTypeTest.php new file mode 100644 index 00000000000..8d93afc9e44 --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/DateArrayTypeTest.php @@ -0,0 +1,55 @@ +dates = $values['dates']; + } +} + +class DateArrayTypeTest extends TestCase +{ + public function testDateTimeTypesInArrays(): void + { + $data = [ + 'dates' => ['2023-01-01', '2023-02-01', '2023-03-01'] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = DateArrayType::fromJson($json); + + $this->assertInstanceOf(DateTime::class, $object->dates[0], 'dates[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dates[0]->format('Y-m-d'), 'dates[0] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[1], 'dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-02-01', $object->dates[1]->format('Y-m-d'), 'dates[1] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[2], 'dates[2] should be a DateTime instance.'); + $this->assertEquals('2023-03-01', $object->dates[2]->format('Y-m-d'), 'dates[2] should have the correct date.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for dates array.'); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/EmptyArraysTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/EmptyArraysTest.php new file mode 100644 index 00000000000..b44f3d093e6 --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/EmptyArraysTest.php @@ -0,0 +1,73 @@ + $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( + array $values, + ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; + } +} + +class EmptyArraysTest extends TestCase +{ + public function testEmptyArrays(): void + { + $data = [ + 'empty_string_array' => [], + 'empty_map_array' => [], + 'empty_dates_array' => [] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = EmptyArraysType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for EmptyArraysType.'); + + // Check that arrays are empty + $this->assertEmpty($object->emptyStringArray, 'empty_string_array should be empty.'); + $this->assertEmpty($object->emptyMapArray, 'empty_map_array should be empty.'); + $this->assertEmpty($object->emptyDatesArray, 'empty_dates_array should be empty.'); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/EnumTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/EnumTest.php new file mode 100644 index 00000000000..ef5b8484dfd --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/EnumTest.php @@ -0,0 +1,76 @@ +value; + } +} + +class ShapeType extends SerializableType +{ + /** + * @var Shape $shape + */ + #[JsonProperty('shape')] + public Shape $shape; + + /** + * @var Shape[] $shapes + */ + #[ArrayType([Shape::class])] + #[JsonProperty('shapes')] + public array $shapes; + + /** + * @param Shape $shape + * @param Shape[] $shapes + */ + public function __construct( + Shape $shape, + array $shapes, + ) { + $this->shape = $shape; + $this->shapes = $shapes; + } +} + +class EnumTest extends TestCase +{ + public function testShapeEnumSerialization(): void + { + $object = new ShapeType( + Shape::Circle, + [Shape::Square, Shape::Circle, Shape::Triangle] + ); + + $expectedJson = json_encode([ + 'shape' => 'CIRCLE', + 'shapes' => ['SQUARE', 'CIRCLE', 'TRIANGLE'] + ], JSON_THROW_ON_ERROR); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString( + $expectedJson, + $serializedJson, + 'Serialized JSON does not match expected JSON for shape and shapes properties.' + ); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/InvalidTypesTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/InvalidTypesTest.php new file mode 100644 index 00000000000..67bfd235b2f --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/InvalidTypesTest.php @@ -0,0 +1,45 @@ +integerProperty = $values['integerProperty']; + } +} + +class InvalidTypesTest extends TestCase +{ + public function testInvalidTypesThrowExceptions(): void + { + // Create test data with invalid type for integer_property (string instead of int) + $data = [ + 'integer_property' => 'not_an_integer' + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $this->expectException(\TypeError::class); + + // Attempt to deserialize invalid data + InvalidType::fromJson($json); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/MixedDateArrayTypeTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/MixedDateArrayTypeTest.php new file mode 100644 index 00000000000..3bf18aec25b --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/MixedDateArrayTypeTest.php @@ -0,0 +1,60 @@ + $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->mixedDates = $values['mixedDates']; + } +} + +class MixedDateArrayTypeTest extends TestCase +{ + public function testDateTimeTypesInUnionArrays(): void + { + $data = [ + 'mixed_dates' => [ + 1 => '2023-01-01T12:00:00+00:00', + 2 => null, + 3 => 'Some String' + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = MixedDateArrayType::fromJson($json); + + $this->assertInstanceOf(DateTime::class, $object->mixedDates[1], 'mixed_dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:00:00', $object->mixedDates[1]->format('Y-m-d H:i:s'), 'mixed_dates[1] should have the correct datetime.'); + + $this->assertNull($object->mixedDates[2], 'mixed_dates[2] should be null.'); + + $this->assertEquals('Some String', $object->mixedDates[3], 'mixed_dates[3] should be "Some String".'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for mixed_dates.'); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/NestedUnionArrayTypeTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/NestedUnionArrayTypeTest.php new file mode 100644 index 00000000000..4667ecafcb9 --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/NestedUnionArrayTypeTest.php @@ -0,0 +1,99 @@ +nestedProperty = $values['nestedProperty']; + } +} + +class NestedUnionArrayType extends SerializableType +{ + /** + * @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( + array $values, + ) { + $this->nestedArray = $values['nestedArray']; + } +} + +class NestedUnionArrayTypeTest extends TestCase +{ + public function testNestedUnionTypesInArrays(): void + { + $data = [ + 'nested_array' => [ + 1 => [ + 1 => ['nested_property' => 'Nested One'], + 2 => null, + 4 => '2023-01-02' + ], + 2 => [ + 5 => ['nested_property' => 'Nested Two'], + 7 => '2023-02-02' + ] + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = NestedUnionArrayType::fromJson($json); + + // Level 1 + $this->assertInstanceOf(TestNestedType::class, $object->nestedArray[1][1], 'nested_array[1][1] should be an instance of TestNestedType.'); + $this->assertEquals('Nested One', $object->nestedArray[1][1]->nestedProperty, 'nested_array[1][1]->nestedProperty should match the original data.'); + + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + + // ensure dates are set with the default time + $this->assertInstanceOf(DateTime::class, $object->nestedArray[1][4], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-01-02T00:00:00+00:00', $object->nestedArray[1][4]->format(Constant::DateTimeFormat), 'nested_array[1][4] should have the correct datetime.'); + + // Level 2 + $this->assertInstanceOf(TestNestedType::class, $object->nestedArray[2][5], 'nested_array[2][5] should be an instance of TestNestedType.'); + $this->assertEquals('Nested Two', $object->nestedArray[2][5]->nestedProperty, 'nested_array[2][5]->nestedProperty should match the original data.'); + + $this->assertInstanceOf(DateTime::class, $object->nestedArray[2][7], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-02-02', $object->nestedArray[2][7]->format('Y-m-d'), 'nested_array[1][4] should have the correct date.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for nested_array.'); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/NullPropertyTypeTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/NullPropertyTypeTest.php new file mode 100644 index 00000000000..134296f56e3 --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/NullPropertyTypeTest.php @@ -0,0 +1,50 @@ +nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; + } +} + +class NullPropertyTypeTest extends TestCase +{ + public function testNullPropertiesAreOmitted(): void + { + $object = new NullPropertyType(["nonNullProperty" => "Test String", "nullProperty" => null]); + + $serializedObject = $object->jsonSerialize(); + + $this->assertArrayHasKey('non_null_property', $serializedObject, 'non_null_property should be present in the serialized JSON.'); + $this->assertArrayNotHasKey('null_property', $serializedObject, 'null_property should be omitted from the serialized JSON.'); + + $this->assertEquals('Test String', $serializedObject['non_null_property'], 'non_null_property should have the correct value.'); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/NullableArrayTypeTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/NullableArrayTypeTest.php new file mode 100644 index 00000000000..bf6345e5c6f --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/NullableArrayTypeTest.php @@ -0,0 +1,50 @@ + $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->nullableStringArray = $values['nullableStringArray']; + } +} + +class NullableArrayTypeTest extends TestCase +{ + public function testNullableTypesInArrays(): void + { + $data = [ + 'nullable_string_array' => ['one', null, 'three'] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = NullableArrayType::fromJson($json); + + $this->assertEquals(['one', null, 'three'], $object->nullableStringArray, 'nullable_string_array should match the original data.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for nullable_string_array.'); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/RawClientTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/RawClientTest.php new file mode 100644 index 00000000000..e01ae63b41a --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/RawClientTest.php @@ -0,0 +1,101 @@ +mockHandler = new MockHandler(); + $handlerStack = HandlerStack::create($this->mockHandler); + $client = new Client(['handler' => $handlerStack]); + $this->rawClient = new RawClient(['client' => $client]); + } + + public function testHeaders(): void + { + $this->mockHandler->append(new Response(200)); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::GET, + ['X-Custom-Header' => 'TestValue'] + ); + + $this->sendRequest($request); + + $lastRequest = $this->mockHandler->getLastRequest(); + assert($lastRequest instanceof RequestInterface); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals('TestValue', $lastRequest->getHeaderLine('X-Custom-Header')); + } + + public function testQueryParameters(): void + { + $this->mockHandler->append(new Response(200)); + + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::GET, + [], + ['param1' => 'value1', 'param2' => ['a', 'b'], 'param3' => 'true'] + ); + + $this->sendRequest($request); + + $lastRequest = $this->mockHandler->getLastRequest(); + assert($lastRequest instanceof RequestInterface); + $this->assertEquals( + 'https://api.example.com/test?param1=value1¶m2=a¶m2=b¶m3=true', + (string)$lastRequest->getUri() + ); + } + + public function testJsonBody(): void + { + $this->mockHandler->append(new Response(200)); + + $body = ['key' => 'value']; + $request = new JsonApiRequest( + $this->baseUrl, + '/test', + HttpMethod::POST, + [], + [], + $body + ); + + $this->sendRequest($request); + + $lastRequest = $this->mockHandler->getLastRequest(); + assert($lastRequest instanceof RequestInterface); + $this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type')); + $this->assertEquals(json_encode($body), (string)$lastRequest->getBody()); + } + + private function sendRequest(BaseApiRequest $request): void + { + try { + $this->rawClient->sendRequest($request); + } catch (\Throwable $e) { + $this->fail('An exception was thrown: ' . $e->getMessage()); + } + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/ScalarTypesTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/ScalarTypesTest.php new file mode 100644 index 00000000000..899e949836c --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/ScalarTypesTest.php @@ -0,0 +1,121 @@ + $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var array $floatArray + */ + #[ArrayType(['float'])] + #[JsonProperty('float_array')] + public array $floatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * otherFloatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * floatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ + public function __construct( + array $values, + ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->otherFloatProperty = $values['otherFloatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->floatArray = $values['floatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; + } +} + +class ScalarTypesTest extends TestCase +{ + public function testAllScalarTypesIncludingFloat(): void + { + // Create test data + $data = [ + 'integer_property' => 42, + 'float_property' => 3.14159, + 'other_float_property' => 3, + 'boolean_property' => true, + 'string_property' => 'Hello, World!', + 'int_float_array' => [1, 2.5, 3, 4.75], + 'float_array' => [1, 2, 3, 4] // ensure we handle "integer-looking" floats + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = ScalarTypesTestType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for ScalarTypesTest.'); + + // Check scalar properties + $this->assertEquals(42, $object->integerProperty, 'integer_property should be 42.'); + $this->assertEquals(3.14159, $object->floatProperty, 'float_property should be 3.14159.'); + $this->assertTrue($object->booleanProperty, 'boolean_property should be true.'); + $this->assertEquals('Hello, World!', $object->stringProperty, 'string_property should be "Hello, World!".'); + $this->assertNull($object->nullableBooleanProperty, 'nullable_boolean_property should be null.'); + + // Check int_float_array + $this->assertEquals([1, 2.5, 3, 4.75], $object->intFloatArray, 'int_float_array should match the original data.'); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/TestTypeTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/TestTypeTest.php new file mode 100644 index 00000000000..8e7ca1b825c --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/TestTypeTest.php @@ -0,0 +1,201 @@ +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( + 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; + } +} + +class TestTypeTest extends TestCase +{ + /** + * Test serialization and deserialization of all types in TestType + */ + 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', + 'datetime_property' => '2023-01-01T12:34:56+00:00', + 'string_array' => ['one', 'two', 'three'], + 'map_property' => ['key1' => 1, 'key2' => 2], + 'object_array' => [ + 1 => ['nested_property' => '2021-07-20'], + 2 => null, // Testing nullable objects in array + ], + 'nested_array' => [ + 1 => [1 => 'value1', 2 => null], // Testing nullable strings in nested array + 2 => [3 => 'value3', 4 => 'value4'] + ], + 'dates_array' => ['2023-01-01', null, '2023-03-01'] // Testing nullable dates in array + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = TestType::fromJson($json); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'The serialized JSON does not match the original JSON.'); + + // Check that nullable property is null and not included in JSON + $this->assertNull($object->nullableProperty, 'Nullable property should be null.'); + // @phpstan-ignore-next-line + $this->assertFalse(array_key_exists('nullable_property', json_decode($serializedJson, true)), 'Nullable property should be omitted from JSON.'); + + // Check date properties + $this->assertInstanceOf(DateTime::class, $object->dateProperty, 'date_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dateProperty->format('Y-m-d'), 'date_property should have the correct date.'); + + $this->assertInstanceOf(DateTime::class, $object->datetimeProperty, 'datetime_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:34:56', $object->datetimeProperty->format('Y-m-d H:i:s'), 'datetime_property should have the correct datetime.'); + + // Check scalar arrays + $this->assertEquals(['one', 'two', 'three'], $object->stringArray, 'string_array should match the original data.'); + $this->assertEquals(['key1' => 1, 'key2' => 2], $object->mapProperty, 'map_property should match the original data.'); + + // Check object array with nullable elements + $this->assertInstanceOf(TestNestedType1::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); + $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 + $this->assertEquals('value1', $object->nestedArray[1][1], 'nested_array[1][1] should match the original data.'); + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + $this->assertEquals('value3', $object->nestedArray[2][3], 'nested_array[2][3] should match the original data.'); + $this->assertEquals('value4', $object->nestedArray[2][4], 'nested_array[2][4] should match the original data.'); + + // Check dates array with nullable DateTime objects + $this->assertInstanceOf(DateTime::class, $object->datesArray[0], 'dates_array[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->datesArray[0]->format('Y-m-d'), 'dates_array[0] should have the correct date.'); + $this->assertNull($object->datesArray[1], 'dates_array[1] should be null.'); + $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.'); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/UnionArrayTypeTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/UnionArrayTypeTest.php new file mode 100644 index 00000000000..8d0998f4b7e --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/UnionArrayTypeTest.php @@ -0,0 +1,56 @@ + $mixedArray + */ + #[ArrayType(['integer' => new Union('string', 'integer', 'null')])] + #[JsonProperty('mixed_array')] + public array $mixedArray; + + /** + * @param array{ + * mixedArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->mixedArray = $values['mixedArray']; + } +} + +class UnionArrayTypeTest extends TestCase +{ + public function testUnionTypesInArrays(): void + { + $data = [ + 'mixed_array' => [ + 1 => 'one', + 2 => 2, + 3 => null + ] + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + + $object = UnionArrayType::fromJson($json); + + $this->assertEquals('one', $object->mixedArray[1], 'mixed_array[1] should be "one".'); + $this->assertEquals(2, $object->mixedArray[2], 'mixed_array[2] should be 2.'); + $this->assertNull($object->mixedArray[3], 'mixed_array[3] should be null.'); + + $serializedJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match original JSON for mixed_array.'); + } +} diff --git a/seed/php-sdk/server-sent-events/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/server-sent-events/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/server-sent-events/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/simple-fhir/src/Core/JsonDecoder.php b/seed/php-sdk/simple-fhir/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/simple-fhir/src/Core/JsonDecoder.php +++ b/seed/php-sdk/simple-fhir/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php b/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php b/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php +++ b/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/simple-fhir/src/Core/SerializableType.php b/seed/php-sdk/simple-fhir/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/simple-fhir/src/Core/SerializableType.php +++ b/seed/php-sdk/simple-fhir/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/simple-fhir/src/Core/Union.php b/seed/php-sdk/simple-fhir/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/simple-fhir/src/Core/Union.php +++ b/seed/php-sdk/simple-fhir/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/simple-fhir/src/Types/BaseResource.php b/seed/php-sdk/simple-fhir/src/Types/BaseResource.php index 505b309bd0d..83ac40eb598 100644 --- a/seed/php-sdk/simple-fhir/src/Types/BaseResource.php +++ b/seed/php-sdk/simple-fhir/src/Types/BaseResource.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class BaseResource extends SerializableType { @@ -15,9 +16,9 @@ class BaseResource extends SerializableType public string $id; /** - * @var array $relatedResources + * @var array $relatedResources */ - #[JsonProperty('related_resources'), ArrayType(['mixed'])] + #[JsonProperty('related_resources'), ArrayType([new Union(Account::class, Patient::class, Practitioner::class, Script::class)])] public array $relatedResources; /** @@ -29,7 +30,7 @@ class BaseResource extends SerializableType /** * @param array{ * id: string, - * relatedResources: array, + * relatedResources: array, * memo: Memo, * } $values */ diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/single-url-environment-default/src/Core/JsonDecoder.php b/seed/php-sdk/single-url-environment-default/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/single-url-environment-default/src/Core/JsonDecoder.php +++ b/seed/php-sdk/single-url-environment-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDecoder.php b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDecoder.php +++ b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 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 @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 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 @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 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 @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 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 @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/streaming-parameter/src/Core/JsonDecoder.php b/seed/php-sdk/streaming-parameter/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/JsonDecoder.php +++ b/seed/php-sdk/streaming-parameter/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php b/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php b/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php +++ b/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php b/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php +++ b/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/streaming-parameter/src/Core/Union.php b/seed/php-sdk/streaming-parameter/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/Union.php +++ b/seed/php-sdk/streaming-parameter/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/streaming/src/Core/JsonDecoder.php b/seed/php-sdk/streaming/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/streaming/src/Core/JsonDecoder.php +++ b/seed/php-sdk/streaming/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/streaming/src/Core/JsonDeserializer.php b/seed/php-sdk/streaming/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/streaming/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/streaming/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/streaming/src/Core/JsonSerializer.php b/seed/php-sdk/streaming/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/streaming/src/Core/JsonSerializer.php +++ b/seed/php-sdk/streaming/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/streaming/src/Core/SerializableType.php b/seed/php-sdk/streaming/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/streaming/src/Core/SerializableType.php +++ b/seed/php-sdk/streaming/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/streaming/src/Core/Union.php b/seed/php-sdk/streaming/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/streaming/src/Core/Union.php +++ b/seed/php-sdk/streaming/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/streaming/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/streaming/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/streaming/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/trace/src/Core/JsonDecoder.php b/seed/php-sdk/trace/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/trace/src/Core/JsonDecoder.php +++ b/seed/php-sdk/trace/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/trace/src/Core/JsonDeserializer.php b/seed/php-sdk/trace/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/trace/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/trace/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/trace/src/Core/JsonSerializer.php b/seed/php-sdk/trace/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/trace/src/Core/JsonSerializer.php +++ b/seed/php-sdk/trace/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/trace/src/Core/SerializableType.php b/seed/php-sdk/trace/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/trace/src/Core/SerializableType.php +++ b/seed/php-sdk/trace/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/trace/src/Core/Union.php b/seed/php-sdk/trace/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/trace/src/Core/Union.php +++ b/seed/php-sdk/trace/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/trace/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/trace/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/trace/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/JsonDecoder.php b/seed/php-sdk/undiscriminated-unions/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/JsonDecoder.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php b/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php b/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php b/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/Union.php b/seed/php-sdk/undiscriminated-unions/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/Union.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/undiscriminated-unions/src/Union/UnionClient.php b/seed/php-sdk/undiscriminated-unions/src/Union/UnionClient.php index fe592455cbc..035279c7d31 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Union/UnionClient.php +++ b/seed/php-sdk/undiscriminated-unions/src/Union/UnionClient.php @@ -7,9 +7,12 @@ use Seed\Exceptions\SeedApiException; use Seed\Core\JsonApiRequest; use Seed\Core\HttpMethod; +use Seed\Core\JsonSerializer; +use Seed\Core\Union; use Seed\Core\JsonDecoder; use JsonException; use Psr\Http\Client\ClientExceptionInterface; +use Seed\Union\Types\KeyType; class UnionClient { @@ -28,15 +31,15 @@ public function __construct( } /** - * @param mixed $request + * @param string|array|int|array|array> $request * @param ?array{ * baseUrl?: string, * } $options - * @return mixed + * @return string|array|int|array|array> * @throws SeedException * @throws SeedApiException */ - public function get(mixed $request, ?array $options = null): mixed + public function get(string|array|int $request, ?array $options = null): string|array|int { try { $response = $this->client->sendRequest( @@ -44,13 +47,13 @@ public function get(mixed $request, ?array $options = null): mixed baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '', path: "", method: HttpMethod::POST, - body: $request, + body: JsonSerializer::serializeUnion($request, new Union('string', ['string'], 'integer', ['integer'], [['integer']])), ), ); $statusCode = $response->getStatusCode(); if ($statusCode >= 200 && $statusCode < 400) { $json = $response->getBody()->getContents(); - return JsonDecoder::decodeMixed($json); + return JsonDecoder::decodeUnion($json, new Union('string', ['string'], 'integer', ['integer'], [['integer']])); // @phpstan-ignore-line } } catch (JsonException $e) { throw new SeedException(message: "Failed to deserialize response: {$e->getMessage()}", previous: $e); @@ -68,7 +71,7 @@ public function get(mixed $request, ?array $options = null): mixed * @param ?array{ * baseUrl?: string, * } $options - * @return array + * @return array|string, string> * @throws SeedException * @throws SeedApiException */ @@ -85,7 +88,7 @@ public function getMetadata(?array $options = null): array $statusCode = $response->getStatusCode(); if ($statusCode >= 200 && $statusCode < 400) { $json = $response->getBody()->getContents(); - return JsonDecoder::decodeArray($json, ['mixed' => 'string']); // @phpstan-ignore-line + return JsonDecoder::decodeArray($json, ['string' => 'string']); // @phpstan-ignore-line } } catch (JsonException $e) { throw new SeedException(message: "Failed to deserialize response: {$e->getMessage()}", previous: $e); diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/unions/src/Core/JsonDecoder.php b/seed/php-sdk/unions/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/unions/src/Core/JsonDecoder.php +++ b/seed/php-sdk/unions/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/unions/src/Core/JsonDeserializer.php b/seed/php-sdk/unions/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/unions/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/unions/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/unions/src/Core/JsonSerializer.php b/seed/php-sdk/unions/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/unions/src/Core/JsonSerializer.php +++ b/seed/php-sdk/unions/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/unions/src/Core/SerializableType.php b/seed/php-sdk/unions/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/unions/src/Core/SerializableType.php +++ b/seed/php-sdk/unions/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/unions/src/Core/Union.php b/seed/php-sdk/unions/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/unions/src/Core/Union.php +++ b/seed/php-sdk/unions/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/unions/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/unions/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/unions/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/unknown/src/Core/JsonDecoder.php b/seed/php-sdk/unknown/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/unknown/src/Core/JsonDecoder.php +++ b/seed/php-sdk/unknown/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/unknown/src/Core/JsonDeserializer.php b/seed/php-sdk/unknown/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/unknown/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/unknown/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/unknown/src/Core/JsonSerializer.php b/seed/php-sdk/unknown/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/unknown/src/Core/JsonSerializer.php +++ b/seed/php-sdk/unknown/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/unknown/src/Core/SerializableType.php b/seed/php-sdk/unknown/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/unknown/src/Core/SerializableType.php +++ b/seed/php-sdk/unknown/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/unknown/src/Core/Union.php b/seed/php-sdk/unknown/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/unknown/src/Core/Union.php +++ b/seed/php-sdk/unknown/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/unknown/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/unknown/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/unknown/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/validation/src/Core/JsonDecoder.php b/seed/php-sdk/validation/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/validation/src/Core/JsonDecoder.php +++ b/seed/php-sdk/validation/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/validation/src/Core/JsonDeserializer.php b/seed/php-sdk/validation/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/validation/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/validation/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/validation/src/Core/JsonSerializer.php b/seed/php-sdk/validation/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/validation/src/Core/JsonSerializer.php +++ b/seed/php-sdk/validation/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/validation/src/Core/SerializableType.php b/seed/php-sdk/validation/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/validation/src/Core/SerializableType.php +++ b/seed/php-sdk/validation/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/validation/src/Core/Union.php b/seed/php-sdk/validation/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/validation/src/Core/Union.php +++ b/seed/php-sdk/validation/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/validation/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/validation/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/validation/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/variables/src/Core/JsonDecoder.php b/seed/php-sdk/variables/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/variables/src/Core/JsonDecoder.php +++ b/seed/php-sdk/variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/variables/src/Core/JsonDeserializer.php b/seed/php-sdk/variables/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/variables/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/variables/src/Core/JsonSerializer.php b/seed/php-sdk/variables/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/variables/src/Core/JsonSerializer.php +++ b/seed/php-sdk/variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/variables/src/Core/SerializableType.php b/seed/php-sdk/variables/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/variables/src/Core/SerializableType.php +++ b/seed/php-sdk/variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/variables/src/Core/Union.php b/seed/php-sdk/variables/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/variables/src/Core/Union.php +++ b/seed/php-sdk/variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/version-no-default/src/Core/JsonDecoder.php b/seed/php-sdk/version-no-default/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/version-no-default/src/Core/JsonDecoder.php +++ b/seed/php-sdk/version-no-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * 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 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/version-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/version-no-default/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * 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 eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/version-no-default/src/Core/JsonSerializer.php +++ b/seed/php-sdk/version-no-default/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * 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 ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/version-no-default/src/Core/SerializableType.php +++ b/seed/php-sdk/version-no-default/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { 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 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/version-no-default/src/Core/Union.php +++ b/seed/php-sdk/version-no-default/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/version/src/Core/JsonDecoder.php b/seed/php-sdk/version/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/version/src/Core/JsonDecoder.php +++ b/seed/php-sdk/version/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/version/src/Core/JsonDeserializer.php b/seed/php-sdk/version/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/version/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/version/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/version/src/Core/JsonSerializer.php b/seed/php-sdk/version/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/version/src/Core/JsonSerializer.php +++ b/seed/php-sdk/version/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/version/src/Core/SerializableType.php b/seed/php-sdk/version/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/version/src/Core/SerializableType.php +++ b/seed/php-sdk/version/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/version/src/Core/Union.php b/seed/php-sdk/version/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/version/src/Core/Union.php +++ b/seed/php-sdk/version/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/version/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/version/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/version/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/websocket/src/Core/JsonDecoder.php b/seed/php-sdk/websocket/src/Core/JsonDecoder.php index 34651d0b9eb..c7f9629e018 100644 --- a/seed/php-sdk/websocket/src/Core/JsonDecoder.php +++ b/seed/php-sdk/websocket/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/websocket/src/Core/JsonDeserializer.php b/seed/php-sdk/websocket/src/Core/JsonDeserializer.php index 4c9998886ee..b1de7d141ac 100644 --- a/seed/php-sdk/websocket/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/websocket/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/websocket/src/Core/JsonSerializer.php b/seed/php-sdk/websocket/src/Core/JsonSerializer.php index eae0c087441..1e37550f15e 100644 --- a/seed/php-sdk/websocket/src/Core/JsonSerializer.php +++ b/seed/php-sdk/websocket/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/websocket/src/Core/SerializableType.php b/seed/php-sdk/websocket/src/Core/SerializableType.php index ecb6c6abc19..9121bdca01c 100644 --- a/seed/php-sdk/websocket/src/Core/SerializableType.php +++ b/seed/php-sdk/websocket/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/websocket/src/Core/Union.php b/seed/php-sdk/websocket/src/Core/Union.php index 8608d2cae49..1e9fe801ee7 100644 --- a/seed/php-sdk/websocket/src/Core/Union.php +++ b/seed/php-sdk/websocket/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/websocket/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/websocket/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 00000000000..e278eb42883 --- /dev/null +++ b/seed/php-sdk/websocket/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +}