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.
Title: Refactor PHP SDK Models with Symfony Serializer via
ZitadelModelBase ClassDescription:
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
ZitadelModelbase 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 globalObjectSerializer). This leads to:Impact:
Refactoring will result in:
Solution / Tasks:
1. Implement
ZitadelModelBase Class:Create an
abstract class ZitadelModelthat encapsulates a pre-configuredSymfony\Component\Serializer\SerializerInterfaceinstance. This class will provide common JSON and array serialization/deserialization methods for all SDK models to inherit.Example
ZitadelModel.php(ensure allusestatements are at the top of the file):2. Update OpenAPI Generator Templates for PHP Models:
Modify the OpenAPI Generator templates for PHP models to:
ZitadelModel.#[Symfony\Component\Serializer\Attribute\SerializedName("jsonKey")]for mapping JSON keys to PHP property names where they differ.$container, custom ser/des logic).Target structure for a generated model (e.g.,
UserServiceUser.php):3. Update SDK Code to Use New Model Methods:
ObjectSerializer::sanitizeForSerialization()andObjectSerializer::deserialize().ZitadelModel:ObjectSerializer::sanitizeForSerialization($instance)->$instance->toArray()or$instance->toJson().ObjectSerializer::deserialize($data, ModelClass::class)->ModelClass::fromJson($jsonData)orModelClass::fromArray($arrayData).ObjectSerializer.Expected Outcomes:
ZitadelModel.ObjectSerializeris no longer used for model transformations.Additional Notes:
composer.jsonincludessymfony/serializer,symfony/property-access,symfony/property-info. Considersymfony/serializer-pack.AbstractObjectNormalizer::SKIP_NULL_VALUEScan be used intoJson/toArrayif specific null omission behavior is required.