Skip to content

Advanced Type Support

This document covers advanced type features including union types, enums, custom classes, generics, and complex type scenarios.

Type Overview

Type CategoryExamplesPHP Type Hint
Scalarstring, int, float, boolNative types
Specialmixed, array, objectNative types
Unionint|string, int|float|stringUnion types (PHP 8.0+)
DTO ReferenceUser, AddressGenerated DTO class
Class\DateTimeImmutable, \Money\MoneyFQCN
Enum\App\Enum\StatusEnum class
Arraystring[], int[], User[]array with PHPDoc
CollectionItem[] with collection: true\ArrayObject with PHPDoc

Union Types

Union types allow a field to accept multiple types. Supported in PHP 8.0+.

Configuration

PHP Builder

php
use PhpCollective\Dto\Config\Field;

// Two types
Field::union('id', 'int', 'string')

// Three types
Field::union('value', 'int', 'float', 'string')

// With modifiers
Field::union('id', 'int', 'string')->required()

// Alternative using of()
Field::of('id', 'int|string')

PHP Array

php
'id' => [
    'type' => 'int|string',
],
'value' => [
    'type' => 'int|float|string',
    'required' => true,
],

XML

xml
<field name="id" type="int|string"/>
<field name="value" type="int|float|string" required="true"/>

YAML

yaml
id:
  type: 'int|string'
value:
  type: 'int|float|string'
  required: true

NEON

ini
id:
    type: int|string
value:
    type: int|float|string
    required: true

Generated Code

php
// Property
protected int|string|null $id = null;

// Getter
public function getId(): int|string|null
{
    return $this->id;
}

// Setter
public function setId(int|string|null $id): self
{
    $this->id = $id;
    $this->_touchedFields['id'] = true;
    return $this;
}

Nullable Union Types

Union types become nullable automatically unless required: true:

php
// Not required (default) - nullable
Field::union('id', 'int', 'string')
// Type: int|string|null

// Required - not nullable
Field::union('id', 'int', 'string')->required()
// Type: int|string

Enums

Both backed enums and unit enums are supported.

Quick Examples

Backed enum from string (recommended):

php
enum OrderStatus: string
{
    case Pending = 'pending';
    case Confirmed = 'confirmed';
}

$dto = new OrderDto(['status' => 'confirmed']);
$dto->getStatus(); // OrderStatus::Confirmed
$dto->toArray();   // ['status' => 'confirmed']

Unit enum from case name:

php
enum Priority
{
    case High;
    case Low;
}

$dto = new TaskDto(['priority' => 'High']);
$dto->getPriority(); // Priority::High

Backed enums have scalar values and are the most common:

php
// Your enum
enum OrderStatus: string
{
    case Pending = 'pending';
    case Confirmed = 'confirmed';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
}

Configuration

php
// PHP Builder
Field::enum('status', \App\Enum\OrderStatus::class)

// PHP Array
'status' => [
    'type' => '\App\Enum\OrderStatus',
],
xml
<!-- XML -->
<field name="status" type="\App\Enum\OrderStatus"/>
yaml
# YAML
status:
  type: '\App\Enum\OrderStatus'

Usage

php
// Set with enum instance
$order->setStatus(OrderStatus::Pending);

// Set from backing value (auto-converted)
$order = new OrderDto(['status' => 'confirmed']);
$order->getStatus(); // OrderStatus::Confirmed

// toArray() returns backing value
$order->toArray(); // ['status' => 'confirmed']

Unit Enums

Unit enums have no backing value:

php
enum Priority
{
    case Low;
    case Medium;
    case High;
}

Usage

php
// Set with enum instance
$task->setPriority(Priority::High);

// Set from case name (string)
$task = new TaskDto(['priority' => 'High']);
$task->getPriority(); // Priority::High

// toArray() returns case name
$task->toArray(); // ['priority' => 'High']

Int-Backed Enums

php
enum HttpStatus: int
{
    case Ok = 200;
    case NotFound = 404;
    case ServerError = 500;
}

// Set from int value
$response = new ResponseDto(['status' => 200]);
$response->getStatus(); // HttpStatus::Ok

Custom Classes

Any PHP class can be used as a field type.

Basic Class Fields

php
// PHP Builder
Field::class('createdAt', \DateTimeImmutable::class)
Field::class('money', \Money\Money::class)

// PHP Array
'createdAt' => [
    'type' => '\DateTimeImmutable',
],
xml
<!-- XML -->
<field name="createdAt" type="\DateTimeImmutable"/>

With Factory Method

When a class needs custom instantiation:

php
Field::class('date', \DateTimeImmutable::class)
    ->factory('createFromFormat')

Field::class('money', \Money\Money::class)
    ->factory('fromArray')

// External factory
Field::class('price', \App\ValueObject\Price::class)
    ->factory('App\Factory\PriceFactory::create')

With Serialization

Control how objects are converted in toArray():

php
// Call toArray() method
Field::class('dimensions', Dimensions::class)
    ->serialize('array')

// Call __toString() method
Field::class('sku', Sku::class)
    ->serialize('string')

// Full round-trip with FromArrayToArrayInterface
Field::class('metadata', Metadata::class)
    ->serialize('FromArrayToArray')

Constructor-Based Creation

If no factory is specified and the class has a single-argument constructor, it will be called automatically:

php
class Wrapper
{
    public function __construct(public mixed $value) {}
}

// No factory needed - constructor called with the value
Field::class('wrapper', Wrapper::class)

DTO References

Reference other DTOs in your schema:

Simple Reference

php
// PHP Builder
Field::dto('address', 'Address')
Field::dto('author', 'User')

// PHP Array
'address' => [
    'type' => 'Address',
],
xml
<!-- XML -->
<field name="address" type="Address"/>

The generator automatically:

  • Resolves the DTO class name (adds namespace and suffix)
  • Handles nested array-to-DTO conversion
  • Supports deep cloning

Nested DTO Conversion

php
$order = new OrderDto([
    'customer' => [
        'name' => 'John',
        'email' => 'john@example.com',
    ],
]);

// Nested array automatically converted to CustomerDto
$order->getCustomer()->getName(); // 'John'

Array Types

Typed Arrays

Arrays with element type hints:

php
// PHP Builder
Field::array('tags', 'string')      // string[]
Field::array('scores', 'int')       // int[]
Field::array('users', 'User')       // User[] (DTO array)

// PHP Array
'tags' => 'string[]',
'scores' => ['type' => 'int[]'],
xml
<!-- XML -->
<field name="tags" type="string[]"/>
<field name="users" type="User[]"/>

Generated PHPDoc

Typed arrays generate generic PHPDoc for static analysis:

php
/**
 * @var array<int, string>
 */
protected array $tags = [];

/**
 * @return array<int, string>
 */
public function getTags(): array

Untyped Arrays

Plain arrays without element type:

php
Field::array('data')  // Just 'array'

// PHP Array
'data' => 'array',

Collections

Collections provide add*(), get*(), has*() methods for managing items.

Basic Collection

php
// PHP Builder
Field::collection('items', 'Item')->singular('item')

// PHP Array
'items' => [
    'type' => 'Item[]',
    'collection' => true,
    'singular' => 'item',
],
xml
<!-- XML -->
<field name="items" type="Item[]" collection="true" singular="item"/>

Generated Methods

php
// Add item
$cart->addItem(new ItemDto(['name' => 'Widget']));

// Get all items (returns ArrayObject)
$items = $cart->getItems();

// Check if has items
if ($cart->hasItems()) { ... }

Associative Collections

Keyed access to collection items:

php
// PHP Builder
Field::collection('settings', 'Setting')
    ->singular('setting')
    ->associative()

// With custom key field
Field::collection('products', 'Product')
    ->singular('product')
    ->associative('sku')
xml
<!-- XML -->
<field name="settings" type="Setting[]" collection="true"
       singular="setting" associative="true"/>
<field name="products" type="Product[]" collection="true"
       singular="product" associative="true" key="sku"/>

Generated Methods (Associative)

php
// Add with key
$config->addSetting('theme', new SettingDto(['value' => 'dark']));

// Get by key
$theme = $config->getSetting('theme');

// Check by key
if ($config->hasSetting('notifications')) { ... }

Custom Collection Type

Override the default \ArrayObject:

php
// PHP Array
'items' => [
    'type' => 'Item[]',
    'collection' => true,
    'singular' => 'item',
    'collectionType' => '\Doctrine\Common\Collections\ArrayCollection',
],
xml
<!-- XML -->
<field name="items" type="Item[]" collection="true"
       singular="item" collectionType="\Doctrine\Common\Collections\ArrayCollection"/>

PHPDoc Generics

The generator creates generic PHPDoc annotations for static analysis tools (PHPStan, Psalm):

Array Generics

php
// Configuration: type="string[]"
// Generated:
/**
 * @var array<int, string>
 */
protected array $tags = [];

// Associative array
/**
 * @var array<string, SettingDto>
 */
protected array $settings = [];

Collection Generics

php
// Configuration: type="Item[]" collection="true"
// Generated:
/**
 * @var \ArrayObject<int, ItemDto>
 */
protected ?\ArrayObject $items = null;

// Associative collection
/**
 * @var \ArrayObject<string, ProductDto>
 */
protected ?\ArrayObject $products = null;

Complex Type Examples

E-commerce Order

php
return Schema::create()
    ->dto(Dto::create('Order')->fields(
        Field::int('id')->required(),
        Field::enum('status', \App\Enum\OrderStatus::class)->required(),
        Field::dto('customer', 'Customer')->required(),
        Field::dto('shippingAddress', 'Address'),
        Field::dto('billingAddress', 'Address'),
        Field::collection('items', 'OrderItem')->singular('item'),
        Field::class('total', \Money\Money::class)
            ->factory('fromArray')
            ->serialize('array'),
        Field::class('createdAt', \DateTimeImmutable::class)->required(),
        Field::class('shippedAt', \DateTimeImmutable::class),
    ))
    ->dto(Dto::create('OrderItem')->fields(
        Field::union('productId', 'int', 'string')->required(),
        Field::string('name')->required(),
        Field::int('quantity')->required()->default(1),
        Field::class('unitPrice', \Money\Money::class)
            ->factory('fromArray')
            ->serialize('array'),
    ))
    ->toArray();

API Response with Metadata

php
return Schema::create()
    ->dto(Dto::create('ApiResponse')->fields(
        Field::bool('success')->required()->default(true),
        Field::mixed('data'),
        Field::array('errors', 'string'),
        Field::dto('pagination', 'Pagination'),
        Field::array('meta'),  // Untyped for flexibility
    ))
    ->dto(Dto::create('Pagination')->fields(
        Field::int('page')->required()->default(1),
        Field::int('perPage')->required()->default(20),
        Field::int('total')->required(),
        Field::int('totalPages')->required(),
        Field::bool('hasMore')->required(),
    ))
    ->toArray();

Event Sourcing

php
return Schema::create()
    ->dto(Dto::immutable('DomainEvent')->fields(
        Field::string('eventId')->required(),
        Field::string('aggregateId')->required(),
        Field::int('version')->required(),
        Field::class('occurredAt', \DateTimeImmutable::class)->required(),
        Field::array('payload'),
    ))
    ->dto(Dto::immutable('OrderPlaced')->extends('DomainEvent')->fields(
        Field::dto('order', 'Order')->required(),
    ))
    ->dto(Dto::immutable('OrderShipped')->extends('DomainEvent')->fields(
        Field::string('trackingNumber')->required(),
        Field::string('carrier')->required(),
    ))
    ->toArray();

Type Coercion

The library performs automatic type coercion in certain cases:

Input TypeTarget TypeBehavior
stringBacked EnumCalls Enum::tryFrom($value)
intBacked Enum (int)Calls Enum::tryFrom($value)
stringUnit EnumCalls constant(Enum::$value)
arrayDTOCalls Dto::createFromArray($value)
arrayClass with fromArrayCalls factory method
mixedClass with constructorCalls new Class($value)

Best Practices

  1. Use specific types - Avoid mixed when a more specific type is possible
  2. Prefer backed enums - They serialize cleanly and work with databases
  3. Use union types sparingly - Too many types reduce type safety
  4. Add factories for value objects - Ensure clean instantiation
  5. Configure serialization - Ensure toArray() produces expected output
  6. Leverage PHPDoc generics - Let static analysis tools help catch errors

Released under the MIT License.