HypervelHypervel
Hypervel
Documentation
GitHub
Hypervel
Documentation
GitHub
  • Documentation

    • Prologue

      • Contributing Guide
    • Getting Started

      • Introduction
      • Installation
      • Configuration
      • Directory Structure
      • Deployment
    • Architecture Concepts

      • Request Lifecycle
      • Service Container
      • Service Providers
      • Facades
    • The Basics

      • Routing
      • Middleware
      • CSRF Protection
      • Controllers
      • Requests
      • Responses
      • Views
      • Blade Templates
      • URL Generation
      • Session
      • Validation
      • Error Handling
      • Logging
    • Digging Deeper

      • Artisan Console
      • Broadcasting
      • Cache
      • Collections
      • Context
      • Coroutine
      • Contracts
      • Data Objects
      • Events
      • File Storage
      • Helpers
      • HTTP Client
      • Localization
      • Mail
      • Notifications
      • Package Development
      • Package Porting
      • Processes
      • Queues
      • Rate Limiting
      • Strings
      • Task Scheduling
    • Security

      • Authentication
      • Authorization
      • Encryption
      • Hashing
    • Database

      • Getting Started
      • Query Builder
      • Pagination
      • Migrations
      • Seeding
      • Redis
    • Eloquent ORM

      • Getting Started
      • Relationships
      • Collections
      • Mutators / Casts
      • API Resources
      • Serialization
      • Factories
    • Testing

      • Getting Started
      • HTTP Tests
      • Console Tests
      • Database
      • Mocking
      • Packages Toolkit
    • Packages

      • Sanctum

Data Objects

  • Introduction
  • Basic Usage
    • Creating a Data Object
    • Creating Instances
    • Property Name Conversion
  • Type Conversion
    • Supported Conversions
    • DateTime Handling
  • Nested Data Objects
    • Basic Nested Objects
    • Deep Nested Objects
  • Array Access and Immutability
    • Reading Properties
    • Array Access Immutability
  • Serialization
    • Array Conversion
    • JSON Serialization
  • Advanced Features
    • Custom Property Conversion
    • Custom Dependencies Resolving
    • Default Values and Nullable Properties
    • Refreshing Data Objects
    • Validation and Error Handling
    • Eloquent Model Integration
  • Best Practices
    • Use Type Declarations
    • Organize Related Data Objects
    • Use AutoResolve Judiciously
    • Handle DateTime Formats Consistently

Introduction

Data Objects provide a powerful and elegant way to create immutable, type-safe data transfer objects in your Hypervel application. They are perfect for handling API responses, configuration data, and any scenario where you need structured data with automatic type conversion and serialization capabilities.

Unlike simple arrays or standard PHP objects, Data Objects offer automatic type casting, property name conversion, nested object resolution, and built-in serialization support while providing array access immutability to prevent accidental modification through array syntax.

Key benefits of Data Objects include:

  • Type Safety: Automatic conversion between data types with validation
  • Array Access Protection: Prevents modification through array access syntax (use readonly properties for full immutability)
  • Property Mapping: Automatic conversion between snake_case and camelCase naming conventions
  • Nested Resolution: Support for automatically resolving nested Data Objects and DateTime instances
  • Serialization: Built-in JSON and array serialization support

Basic Usage

Creating a Data Object

To create a Data Object, extend the Hypervel\Support\DataObject class and define your constructor with typed properties:

<?php

use Hypervel\Support\DataObject;

class UserDataObject extends DataObject
{
    public function __construct(
        public string $name,
        public int $age,
        public string $email,
        public ?string $phone = null
    ) {}
}

Creating Instances

Use the static make method to create instances from arrays:

$userData = [
    'name' => 'John Doe',
    'age' => '30',        // String will be converted to int
    'email' => '[email protected]',
    'phone' => null
];

$user = UserDataObject::make($userData);

$user->name;  // 'John Doe'
$user->age;   // 30 (integer)
$user->email; // '[email protected]'
$user->phone; // null

Property Name Conversion

Data Objects automatically convert between snake_case data keys and camelCase properties:

class ProductDataObject extends DataObject
{
    public function __construct(
        public string $productName,      // Maps to 'product_name' in array
        public float $unitPrice,         // Maps to 'unit_price' in array
        public bool $isAvailable         // Maps to 'is_available' in array
    ) {}
}

$product = ProductDataObject::make([
    'product_name' => 'Laptop',
    'unit_price' => '999.99',
    'is_available' => 1              // Will be converted to true
]);

$product->productName;  // 'Laptop'
$product->unitPrice;    // 999.99 (float)
$product->isAvailable;  // true (boolean)

$product['product_name'];  // 'Laptop'
$product['unit_price'];    // 999.99 (float)
$product['is_available'];  // true (boolean)

Type Conversion

Data Objects automatically convert data types based on your property declarations:

Supported Conversions

class TypeConversionExample extends DataObject
{
    public function __construct(
        public string $stringValue,
        public int $intValue,
        public float $floatValue,
        public bool $boolValue,
        public array $arrayValue
    ) {}
}

$data = TypeConversionExample::make([
    'string_value' => 123,           // Converted to '123'
    'int_value' => '42.99',         // Converted to 42 (truncated)
    'float_value' => '3.14159',     // Converted to 3.14159
    'bool_value' => '0',            // Converted to false
    'array_value' => 'single item'  // Wrapped in ['single item']
]);

DateTime Handling

Data Objects provide sophisticated DateTime handling:

class EventDataObject extends DataObject
{
    public function __construct(
        public string $title,
        public DateTime $createdAt
    ) {}
}

$event = EventDataObject::make([
    'title' => 'Conference',
    'created_at' => '2023-12-25 10:00:00'  // Converted to DateTime
], true); // autoResolve = true for DateTime conversion

Nested Data Objects

Basic Nested Objects

Data Objects support automatic resolution of nested objects when using autoResolve = true:

class AddressDataObject extends DataObject
{
    public function __construct(
        public string $street,
        public string $city,
        public string $zipCode
    ) {}
}

class UserDataObject extends DataObject
{
    public function __construct(
        public string $name,
        public AddressDataObject|array|null $address
    ) {}
}

$userData = [
    'name' => 'John Doe',
    'address' => [
        'street' => '123 Main St',
        'city' => 'Boston',
        'zip_code' => '02101'
    ]
];

// Without autoResolve (default)
$user1 = UserDataObject::make($userData);
// $user1->address is an array

// With autoResolve
$user2 = UserDataObject::make($userData, true);
// $user2->address is an AddressDataObject instance
echo $user2->address->street; // '123 Main St'

Deep Nested Objects

AutoResolve works recursively for deeply nested structures:

class CompanyDataObject extends DataObject
{
    public function __construct(
        public string $name,
        public UserDataObject|array $employee
    ) {}
}

$companyData = [
    'name' => 'Acme Corp',
    'employee' => [
        'name' => 'Jane Smith',
        'address' => [
            'street' => '456 Oak Ave',
            'city' => 'Boston',
            'zip_code' => '02101'
        ]
    ]
];

$company = CompanyDataObject::make($companyData, true);

// All nested objects are automatically resolved
$company->employee->name;                // 'Jane Smith'
$company->employee->address->street;     // '456 Oak Ave'

Array Access and Immutability

Data Objects implement ArrayAccess for convenient property access while maintaining immutability:

Reading Properties

$user = UserDataObject::make($userData);

// Both syntaxes work for reading
$user->name;        // Object property access
$user['name'];      // Array access

// Check if property exists
if (isset($user['email'])) {
    echo $user['email'];
}

Array Access Immutability

$user = UserDataObject::make($userData);

// Direct property assignment works (properties are mutable)
$user->name = 'Jane Doe';  // ✅ This works

// Array access is read-only for immutability
$user['name'] = 'Jane Doe';  // ❌ LogicException
unset($user['name']);        // ❌ LogicException

Tips

For true immutability, declare your properties as readonly:

class ImmutableUserDataObject extends DataObject
{
    public function __construct(
        public readonly string $name,
        public readonly int $age,
        public readonly string $email
    ) {}
}

Serialization

Array Conversion

Convert Data Objects to arrays for database storage or API responses:

$user = UserDataObject::make($userData, true);
$array = $user->toArray();

// Nested objects are also converted to arrays
print_r($array);
// [
//     'name' => 'John Doe',
//     'address' => [
//         'street' => '123 Main St',
//         'city' => 'Boston',
//         'zip_code' => '02101'
//     ]
// ]

JSON Serialization

Data Objects are automatically JSON serializable:

$user = UserDataObject::make($userData, true);

// Direct JSON encoding
$json = json_encode($user);

// In HTTP responses
return response()->json($user);

Advanced Features

Custom Property Conversion

Override the default naming convention conversion methods:

class CustomDataObject extends DataObject
{
    public function __construct(
        public string $snake_case_param,
        public string $multi_word_parameter_name
    ) {}

    /**
     * Convert property name to data key format.
     */
    public static function convertPropertyToDataKey(string $input): string
    {
        return Str::camel($input);  // camelCase to camelCase
    }

    /**
     * Convert data key to property name format.
     */
    public static function convertDataKeyToProperty(string $input): string
    {
        return Str::snake($input);  // camelCase to snake_case
    }
}

$object = CustomDataObject::make([
    'snakeCaseParam' => 'foo',           // Maps to snake_case_param
    'multiWordParameterName' => 'bar'    // Maps to multi_word_parameter_name
]);

Custom Dependencies Resolving

You can customize how specific types are resolved when using autoResolve = true by overriding the getCustomizedDependencies method:

use Money\Money;
use Money\Currency;

class OrderDataObject extends DataObject
{
    public function __construct(
        public string $orderNumber,
        public Money $totalAmount,
        public CustomStatus $status
    ) {}

    /**
     * Define custom resolvers for specific types.
     */
    protected static function getCustomizedDependencies(): array
    {
        return array_merge(parent::getCustomizedDependencies(), [
            // Custom Money object resolver
            Money::class => function ($value) {
                if ($value instanceof Money) {
                    return $value;
                }

                // Expect array with 'amount' and 'currency'
                if (is_array($value)) {
                    return new Money(
                        $value['amount'] ?? 0,
                        new Currency($value['currency'] ?? 'USD')
                    );
                }

                return new Money(0, new Currency('USD'));
            },

            // Custom status enum resolver
            CustomStatus::class => function ($value) {
                if ($value instanceof CustomStatus) {
                    return $value;
                }

                return CustomStatus::from($value);
            },
        ]);
    }
}

$order = OrderDataObject::make([
    'order_number' => 'ORD-12345',
    'total_amount' => [
        'amount' => 2999,  // $29.99 in cents
        'currency' => 'USD'
    ],
    'status' => 'pending'
], true);

$order->totalAmount->getAmount();    // 2999
$order->status->value;               // 'pending'

The getCustomizedDependencies method returns an array where:

  • Keys are the fully qualified class names or interface names
  • Values are callable functions that handle the conversion logic

Built-in dependencies include:

  • DateTimeInterface::class
  • CarbonInterface::class
  • DateTime::class
  • Carbon::class

Tips

Custom dependency resolvers are called recursively, so they work with deeply nested Data Objects. Always merge with parent::getCustomizedDependencies() to preserve built-in DateTime handling.

Default Values and Nullable Properties

class UserDataObject extends DataObject
{
    public function __construct(
        public string $name,
        public string $status = 'active',      // Default value
        public ?string $nickname = null,       // Nullable
        public array $preferences = []         // Default array
    ) {}
}

$user = UserDataObject::make([
    'name' => 'John Doe'
    // Other properties will use defaults
]);

$user->status;      // 'active'
$user->nickname;    // null
count($user->preferences); // 0

Refreshing Data Objects

Once you change the property values in data objects, you need to clear internal caches manually to prevent getting old results:

$user = UserDataObject::make([
    'name' => 'Original Name',
    'age' => 30,
]);

// Modify properties
$user->name = 'Updated Name';
$user->age = 35;

$user->toArray(); // This will return old result

// Refresh internal caches
$user->refresh();

Validation and Error Handling

Data Objects throw exceptions for missing required properties:

try {
    $user = UserDataObject::make([
        // Missing required 'name' property
        'age' => 30
    ]);
} catch (RuntimeException $e) {
    echo $e->getMessage();
    // "Missing required property `name` in `App\DataObjects\UserDataObject`"
}

Eloquent Model Integration

Use the AsDataObject cast to automatically convert JSON database columns to Data Objects in your Eloquent models:

use Hypervel\Database\Eloquent\Casts\AsDataObject;
use Hypervel\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Get the attributes that should be cast.
     */
    protected function casts(): array
    {
        return [
            'profile' => AsDataObject::castUsing(UserProfileDataObject::class),
            'settings' => AsDataObject::castUsing(UserSettingsDataObject::class),
            'address' => AsDataObject::castUsing(AddressDataObject::class),
        ];
    }
}

Using the cast in practice:

// Create a user with Data Object attributes
$user = User::create([
    'name' => 'John Doe',
    'email' => '[email protected]',
    'profile' => [
        'first_name' => 'John',
        'last_name' => 'Doe',
        'bio' => 'Software Developer',
        'avatar' => 'avatar.jpg'
    ],
    'settings' => [
        'email_notifications' => true,
        'sms_notifications' => false,
        'timezone' => 'America/New_York',
        'theme' => 'dark'
    ]
]);

// Access as Data Objects with full type safety
$user->profile->firstName;        // 'John'
$user->profile->lastName;         // 'Doe'
$user->settings->emailNotifications; // true
$user->settings->timezone;        // 'America/New_York'

// Modify Data Object properties
$user->settings->theme = 'light';
$user->profile->bio = 'Senior Software Developer';
$user->save();

// Database storage
// The 'profile' column stores: {"first_name":"John","last_name":"Doe","bio":"Senior Software Developer","avatar":"avatar.jpg"}
// The 'settings' column stores: {"email_notifications":true,"sms_notifications":false,"timezone":"America/New_York","theme":"light"}

Migration for JSON columns:

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->json('profile')->nullable();
    $table->json('settings')->nullable();
    $table->json('address')->nullable();
    $table->timestamps();
});

Benefits of using AsDataObject cast:

  • Automatic serialization/deserialization: Arrays are automatically converted to/from Data Objects
  • Type safety: Full IDE support and type checking for nested data
  • AutoResolve enabled: Nested Data Objects and DateTime instances are automatically resolved
  • Seamless ORM integration: Works transparently with Eloquent's attribute system

Tips

The AsDataObject cast automatically enables autoResolve = true, so nested Data Objects and DateTime instances are automatically resolved from the JSON data.

Best Practices

Use Type Declarations

Always use proper type declarations for better type safety and IDE support:

// ✅ Good - explicit types
public function __construct(
    public string $name,
    public int $age,
    public ?DateTime $birthDate = null
) {}

// ❌ Avoid - no type declarations
public function __construct(
    public $name,
    public $age,
    public $birthDate = null
) {}

Organize Related Data Objects

Group related Data Objects in dedicated directories:

app/
├── DataObjects/
│   ├── User/
│   │   ├── UserDataObject.php
│   │   ├── AddressDataObject.php
│   │   └── PreferencesDataObject.php
│   └── Product/
│       ├── ProductDataObject.php
│       └── CategoryDataObject.php

Use AutoResolve Judiciously

Enable autoResolve only when you need nested object resolution:

// For simple data transfer
$user = UserDataObject::make($data);           // autoResolve = false

// For complex nested structures
$user = UserDataObject::make($data, true);     // autoResolve = true

Handle DateTime Formats Consistently

Define a consistent DateTime format across your application:

class BaseDataObject extends DataObject
{
    protected string $dateFormat = 'Y-m-d H:i:s';
}

// Extend from BaseDataObject for consistency
class UserDataObject extends BaseDataObject
{
    // ...
}
Edit this page
Last Updated:
Contributors: Albert Chen
Prev
Contracts
Next
Events