Skip to content

Fixed the messy auto-generated serde logic in the library #21

@mridang

Description

@mridang

Title: Refactor PHP SDK Models with Symfony Serializer via ZitadelModel Base Class

Description:
This initiative will refactor the PHP SDK's model classes to leverage the Symfony Serializer component. The goal is to significantly reduce boilerplate, improve maintainability, and adopt a more standard approach to JSON serialization and deserialization by introducing a common ZitadelModel base class and updating model generation templates.

Problem:

The current PHP models, generated by OpenAPI Generator, include substantial duplicated code for serialization/deserialization (e.g., static metadata arrays like $openAPITypes, $attributeMap, and reliance on a custom global ObjectSerializer). This leads to:

  • Increased maintenance overhead for models.
  • Less idiomatic PHP and a steeper learning curve.
  • Potential inconsistencies compared to using a mature, standard serialization library.

Impact:

Refactoring will result in:

  • Slimmer, cleaner, and more maintainable model classes.
  • Easier adoption of modern PHP features within models.
  • Standardized and robust JSON handling powered by Symfony Serializer.
  • Reduced risk of bugs associated with custom serialization logic.
  • Improved developer experience when working with SDK models.

Solution / Tasks:

1. Implement ZitadelModel Base Class:
Create an abstract class ZitadelModel that encapsulates a pre-configured Symfony\Component\Serializer\SerializerInterface instance. This class will provide common JSON and array serialization/deserialization methods for all SDK models to inherit.

Example ZitadelModel.php (ensure all use statements are at the top of the file):

<?php
namespace Zitadel\Client\Model; // Or your chosen SDK namespace

use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\UidNormalizer; // If using Symfony Uid components
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; // For context keys

abstract class ZitadelModel
{
    private static ?SerializerInterface $serializer = null;

    protected static function getSerializer(): SerializerInterface
    {
        if (self::$serializer === null) {
            $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
            $nameConverter = new MetadataAwareNameConverter($classMetadataFactory);
            $reflectionExtractor = new ReflectionExtractor(); // Define once
            $phpDocExtractor = new PhpDocExtractor(); // Define once
            $propertyInfoExtractor = new PropertyInfoExtractor(
                [$reflectionExtractor], // list extractors
                [$phpDocExtractor, $reflectionExtractor], // type extractors
                [$phpDocExtractor], // description extractors
                [$reflectionExtractor], // access extractors
                [$reflectionExtractor]  // mutator extractors
            );
            $normalizers = [
                new DateTimeNormalizer(),
                new UidNormalizer(), // Optional, if using UIDs
                new ArrayDenormalizer(),
                new ObjectNormalizer(
                    $classMetadataFactory, $nameConverter, null, $propertyInfoExtractor
                )
            ];
            $encoders = [new JsonEncoder()];
            self::$serializer = new Serializer($normalizers, $encoders);
        }
        return self::$serializer;
    }

    public function toJson(array $context = []): string
    {
        // Example: $context[AbstractObjectNormalizer::SKIP_NULL_VALUES] = true;
        return static::getSerializer()->serialize($this, JsonEncoder::FORMAT, $context);
    }

    public static function fromJson(string $jsonString, array $context = []): static
    {
        return static::getSerializer()->deserialize($jsonString, static::class, JsonEncoder::FORMAT, $context);
    }

    public function toArray(array $context = []): array
    {
        $data = static::getSerializer()->normalize($this, null, $context);
        return is_array($data) ? $data : (array) $data; // Ensure array output
    }

    public static function fromArray(array $data, array $context = []): static
    {
        return static::getSerializer()->denormalize($data, static::class, null, $context);
    }
}

2. Update OpenAPI Generator Templates for PHP Models:
Modify the OpenAPI Generator templates for PHP models to:

  • Make generated models extend ZitadelModel.
  • Generate PHP typed properties (PHP 7.4+) for all model fields, ideally using constructor property promotion (PHP 8.0+).
  • Use #[Symfony\Component\Serializer\Attribute\SerializedName("jsonKey")] for mapping JSON keys to PHP property names where they differ.
  • Remove the old boilerplate code (static metadata arrays, $container, custom ser/des logic).
  • Ensure enums are generated as PHP 8.1+ backed enums if possible, or as simple string/int properties compatible with Symfony Serializer.

Target structure for a generated model (e.g., UserServiceUser.php):

<?php
namespace Zitadel\Client\Model; // Or your chosen SDK namespace

use Symfony\Component\Serializer\Attribute\SerializedName;
// Assuming other models (UserServiceDetails etc.) also extend ZitadelModel
// Assuming UserServiceUserState is an Enum or another ZitadelModel

class UserServiceUser extends ZitadelModel
{
    public function __construct(
        #[SerializedName("userId")]
        public ?string $userId = null,
        public ?UserServiceDetails $details = null,
        public ?UserServiceUserState $state = null, // Or your Enum type
        public ?string $username = null,
        #[SerializedName("loginNames")]
        /** @var string[]|null */ // PHPDoc for generics if needed
        public ?array $loginNames = null,
        #[SerializedName("preferredLoginName")]
        public ?string $preferredLoginName = null,
        public ?UserServiceHumanUser $human = null,
        public ?UserServiceMachineUser $machine = null
    ) {}
}

3. Update SDK Code to Use New Model Methods:

  • Search the SDK codebase for usages of the old global ObjectSerializer::sanitizeForSerialization() and ObjectSerializer::deserialize().
  • Replace these calls with the new methods from ZitadelModel:
    • ObjectSerializer::sanitizeForSerialization($instance) -> $instance->toArray() or $instance->toJson().
    • ObjectSerializer::deserialize($data, ModelClass::class) -> ModelClass::fromJson($jsonData) or ModelClass::fromArray($arrayData).
  • The aim is to eliminate the models' dependency on ObjectSerializer.

Expected Outcomes:

  • PHP SDK models are significantly leaner, primarily containing property definitions and inheriting ser/des logic.
  • JSON serialization and deserialization are handled robustly by the Symfony Serializer component, configured in ZitadelModel.
  • The custom global ObjectSerializer is no longer used for model transformations.
  • The SDK is easier to maintain and aligns better with modern PHP practices.
  • Functional equivalence for JSON-based API interactions is preserved.

Additional Notes:

  • Dependencies: Ensure composer.json includes symfony/serializer, symfony/property-access, symfony/property-info. Consider symfony/serializer-pack.
  • Null Handling: The default behavior of Symfony Serializer regarding nulls should be reviewed. Context options like AbstractObjectNormalizer::SKIP_NULL_VALUES can be used in toJson/toArray if specific null omission behavior is required.
  • Testing: Thorough testing of serialization/deserialization for various model types (including nested objects, arrays, and different data types) will be crucial.
  • Discriminators: Functionality related to discriminators is explicitly out of scope for this task and can be addressed separately if needed.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request
No fields configured for Enhancement.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions