Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(php): undiscriminated unions #4783

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions generators/php/codegen/src/AsIs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
42 changes: 32 additions & 10 deletions generators/php/codegen/src/asIs/JsonDecoder.Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -106,22 +112,37 @@ public static function decodeInt(string $json): int {
* @return mixed[]|array<string, mixed> 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);
}
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 {
public static function decodeMixed(string $json): mixed
{
return self::decode($json);
}

Expand All @@ -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);
}
}
}
37 changes: 26 additions & 11 deletions generators/php/codegen/src/asIs/JsonDeserializer.Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,28 +66,43 @@ 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);
}

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.
*
Expand Down
33 changes: 25 additions & 8 deletions generators/php/codegen/src/asIs/JsonSerializer.Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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.
*
Expand Down
16 changes: 15 additions & 1 deletion generators/php/codegen/src/asIs/SerializableType.Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -162,4 +176,4 @@ private static function getJsonKey(ReflectionProperty $property): ?string
$jsonPropertyAttr = $property->getAttributes(JsonProperty::class)[0] ?? null;
return $jsonPropertyAttr?->newInstance()?->name;
}
}
}
55 changes: 48 additions & 7 deletions generators/php/codegen/src/asIs/Union.Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string|Union|array<mixed>> 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<mixed> ...$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));
}
}

}
Loading
Loading