BuilderClassPostProcessor

$generator = new ModelGenerator();
$generator->addPostProcessor(new BuilderClassPostProcessor());

The BuilderClassPostProcessor generates a builder class for each model class generated from your schemas. The generated builder classes can be used to populate the object gradually (e.g. building a response object). Additionally, a validate method is added to the builder classes which converts the builder class into the corresponding model class and performs a full validation. The builder class always shares the namespace with it’s corresponding model class.

Hint

If your model contains readonly properties or immutability is enabled, the builder class of course will generate setter methods independent of those settings.

Let’s have a look at a simple object and the generated classes:

{
    "$id": "example",
    "type": "object",
    "properties": {
        "example": {
            "type": "string"
        }
    },
    "required": [
        "example"
    ]
}

In this case the model generator will generate two classes: Example and ExampleBuilder. Generated interfaces:

// class ExampleBuilder
public function setExample(string $members): static;
public function getExample(): ?string;

public function validate(): Example;

// class Example
public function setExample(string $members): static;
public function getExample(): string;

Note, that the getExample method of the ExampleBuilder can return null. This applies for all getter methods of the builder instance, as we don’t know if the property has already been set on the object. In contrast to the general model class, which is fully populated via an array provided in the constructor, the constructor of the builder class accepts any subset of the properties. The remaining properties can be populated via the generated setter methods. Neither the constructor nor the setter methods perform further validation, so if your property has for example a minLength constraint and the value you set doesn’t fulfill the constraint, the set will not fail. The call to validate will populate a new instance of Example and perform a full validation of all constraints and, in case of violations, throw an exception matching your error handling configuration.

If we want to implement the builder as a builder for responses, a full code example might look like the following (assuming serialization is enabled):

$builder = new ExampleBuilder();
$builder->setExample('123abc');

// this call will throw an exception on violations against the JSON schema
$responseBody = $builder->validate();
$response = new Response($responseBody->toJSON());

Nested objects

When your schema provides nested objects, you have different options to populate the nested object in the builder class. As the BuilderClassPostProcessor generates a builder class for each generated model class, you can of course simply use the builder class to populate the nested object. In this case, you don’t need to perform the validation yourself but leave it for the main call to the parents validate method. Nevertheless, you can validate the object and pass a fully validated instance of the nested object (or, if you haven’t used the builder but instantiated the object in a different way, that’s also perfectly fine). As a third option, you can simply pass an array with the values for the nested object.

{
    "$id": "location",
    "type": "object",
    "properties": {
        "coordinates": {
            "$id": "coordinates",
            "type": "object",
            "properties": {
                "latitude": {
                    "type": "string"
                },
                "longitude": {
                    "type": "string"
                },
            },
            "required": [
                "latitude",
                "longitude"
            ]
        }
    }
}

In this case the model generator will generate four classes with the following interfaces:

// class CoordinatesBuilder
public function setLatitude(string $latitude): static;
public function setLongitude(string $longitude): static;
public function getLatitude(): ?string;
public function getLongitude(): ?string;

public function validate(): Coordinates;

// class Coordinates
public function setLatitude(string $latitude): static;
public function setLongitude(string $longitude): static;
public function getLatitude(): string;
public function getLongitude(): string;

// class LocationBuilder

// $coordinates accepts an instance of Coordinates, CoordinatesBuilder or an array.
// If an array is passed, the keys 'latitude' and 'longitude' must be present for a successful validation
public function setCoordinates($coordinates): static;
// returns, whatever you passed to setCoordinates, or null, if you haven't called setCoordinates yet
public function getCoordinates();

public function validate(): Location;

// class Location
public function setCoordinates(Coordinates $coordinates): static;
public function getCoordinates(): ?Coordinates;

Let’s have a look at the usage of the generated classes with the different approaches of populating the Coordinates on the LocationBuilder:

$latitude = '53°7\'6"N';
$longitude = '7°27\'43"E';
$locationBuilder = new LocationBuilder();

// option 1: passing an array with the data
$locationBuilder->setCoordinates(['latitude' => $latitude, 'longitude' => $longitude]);

// option 2: passing an instance of the CoordinatesBuilder
$coordinatesBuilder = new CoordinatesBuilder();
$coordinatesBuilder->setLatitude($latitude);
$coordinatesBuilder->setLongitude($longitude);
$locationBuilder->setCoordinates($coordinatesBuilder);

// option 3: passing an instance of Coordinates,
// either by manually validating the builder or by instantiating it directly.
// Both options might throw exceptions if the data is not valid for the Coordinates class
$locationBuilder->setCoordinates($coordinatesBuilder->validate());
$locationBuilder->setCoordinates(new Coordinates(['latitude' => $latitude, 'longitude' => $longitude]));

The same behaviour applies, if the property of the parent object holds an array of nested objects. In this case, each element of the nested array might use any of the possible options. The call to the validate method on the parent object will cause all elements to be instantiated with the corresponding model class.