PHP Structure helper with typed properties and attributes

Installs: 8

Dependents: 0

Suggesters: 0

Security: 0

Stars: 2

Watchers: 1

Forks: 0

Open Issues: 0

pkg:composer/tommyknocker/struct

This package is auto-updated.

Last update: 2025-10-26 03:23:44 UTC


README

CI PHPStan Level 9 PHP Version License: MIT

A lightweight, type-safe structure helper for PHP 8.1+.
Define your data models with attributes, get automatic validation, array access, and JSON serialization.

πŸš€ Why Struct?

Instead of manually validating arrays, you can define a strict data model with attributes. This makes your code:

  • βœ… Type-safe with runtime validation
  • πŸ”’ Immutable with readonly properties
  • πŸ“¦ Serializable with built-in JSON support
  • 🎯 Simple with minimal boilerplate

πŸ“¦ Installation

Install via Composer:

composer require tommyknocker/struct

Requirements:

  • PHP 8.1 or higher
  • Composer

✨ Features

  • 🏷️ Attribute-based field definitions – Clean and declarative syntax
  • βœ… Advanced type validation – Scalars, objects, arrays, enums, DateTime, union types
  • πŸ”’ Immutability – readonly properties by design
  • 🌐 JSON support – toJson(), fromJson(), JsonSerializable
  • πŸ”„ Array conversion – toArray() with recursive support
  • πŸ“ Default values – Optional fields with defaults
  • πŸ”‘ Field aliases – Map different key names
  • βœ”οΈ Flexible validation system – Custom validators, validation rules, and transformers
  • 🎭 Mixed type support – Handle dynamic data
  • ⏰ DateTime parsing – Automatic string to DateTime conversion
  • πŸ” Cloning with modifications – with() method
  • πŸ“Š ArrayAccess – Array-like read access
  • 🧰 PSR-11 container integration – DI support
  • 🏭 Factory pattern – Centralized struct creation with dependency injection
  • πŸ” PHPStan Level 9 – Maximum static analysis
  • πŸ§ͺ 100% tested – PHPUnit coverage
  • ⚑ Performance optimized – Reflection caching and metadata system
  • πŸ› οΈ Attribute Helper – Automatic Field attribute generation with intelligent type inference

🎯 Use Cases

Perfect for:

  • πŸ“± REST API validation for mobile apps with flexible field types
  • πŸ”„ Data Transfer Objects (DTOs) in clean architecture with validation rules
  • 🌐 Third-party API integration with field mapping and transformations
  • βœ… Form validation with complex rules and data processing
  • πŸ“Š Data serialization/deserialization with custom formats
  • πŸ›‘οΈ Type-safe data handling in microservices with union types
  • 🏭 Enterprise applications with centralized struct creation and dependency injection
  • πŸ” Data processing pipelines with automatic transformations and validation

πŸ‘‰ See practical examples for mobile app REST API scenarios

πŸ“š Examples

Basic Usage: Scalars

use tommyknocker\struct\Struct;
use tommyknocker\struct\Field;

final class Hit extends Struct
{
    #[Field('string')]
    public readonly string $date;

    #[Field('int')]
    public readonly int $type;

    #[Field('string')]
    public readonly string $ip;

    #[Field('string')]
    public readonly string $uuid;

    #[Field('string')]
    public readonly string $referer;
}

$hit = new Hit([
    'date' => '2025-10-09',
    'type' => 1,
    'ip' => '127.0.0.1',
    'uuid' => '7185bbe3-cdd7-4154-88c3-c63416a76327',
    'referer' => 'https://google.com',
]);

echo $hit->date; // 2025-10-09
echo $hit['ip']; // 127.0.0.1 (ArrayAccess support)

Nullable Fields

final class Person extends Struct
{
    #[Field('string')]
    public readonly string $name;

    #[Field('int', nullable: true)]
    public readonly ?int $age;
}

$person = new Person(['name' => 'Alice', 'age' => null]);

Default Values

final class Config extends Struct
{
    #[Field('string', default: 'localhost')]
    public readonly string $host;

    #[Field('int', default: 3306)]
    public readonly int $port;
}

// Both fields use defaults
$config = new Config([]);
echo $config->host; // localhost
echo $config->port; // 3306

Field Aliases

final class User extends Struct
{
    #[Field('string', alias: 'user_name')]
    public readonly string $name;

    #[Field('string', alias: 'email_address')]
    public readonly string $email;
}

// Use API keys as they come
$user = new User([
    'user_name' => 'John',
    'email_address' => 'john@example.com'
]);

Union Types

final class FlexibleField extends Struct
{
    #[Field(['string', 'int'])]
    public readonly string|int $value;
}

$flexible = new FlexibleField(['value' => 'hello']); // βœ… String
$flexible2 = new FlexibleField(['value' => 42]);     // βœ… Integer
// new FlexibleField(['value' => 3.14]); // ❌ Float not allowed

Advanced Validation Rules

use tommyknocker\struct\validation\rules\EmailRule;
use tommyknocker\struct\validation\rules\RangeRule;

final class UserProfile extends Struct
{
    #[Field('string', validationRules: [new EmailRule()])]
    public readonly string $email;

    #[Field('int', validationRules: [new RangeRule(18, 120)])]
    public readonly int $age;
}

$profile = new UserProfile([
    'email' => 'user@example.com',
    'age' => 25
]); // βœ… Valid

Data Transformations

use tommyknocker\struct\transformation\StringToUpperTransformer;

final class ProcessedData extends Struct
{
    #[Field('string', transformers: [new StringToUpperTransformer()])]
    public readonly string $name;
}

$data = new ProcessedData(['name' => 'john doe']);
echo $data->name; // JOHN DOE

Custom Validation (Legacy Support)

class EmailValidator
{
    public static function validate(mixed $value): bool|string
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            return "Invalid email format";
        }
        return true;
    }
}

final class Contact extends Struct
{
    #[Field('string', validator: EmailValidator::class)]
    public readonly string $email;
}

$contact = new Contact(['email' => 'test@example.com']); // βœ… OK
// new Contact(['email' => 'invalid']); // ❌ Throws ValidationException

DateTime Support

final class Event extends Struct
{
    #[Field('string')]
    public readonly string $name;

    #[Field(\DateTimeImmutable::class)]
    public readonly \DateTimeImmutable $date;
}

// Accepts string or DateTime
$event = new Event([
    'name' => 'Conference',
    'date' => '2025-12-31 10:00:00'
]);

Mixed Type Support

final class Payload extends Struct
{
    #[Field('string')]
    public readonly string $type;

    #[Field('mixed')]
    public readonly mixed $data; // Can be anything
}

Nested Objects

final class Address extends Struct
{
    #[Field('string')]
    public readonly string $city;

    #[Field('string')]
    public readonly string $street;
}

final class User extends Struct
{
    #[Field('string')]
    public readonly string $name;

    #[Field(Address::class)]
    public readonly Address $address;
}

$user = new User([
    'name' => 'Bob',
    'address' => ['city' => 'Berlin', 'street' => 'Unter den Linden'],
]);

Arrays of Objects

final class UserWithHistory extends Struct
{
    #[Field('string')]
    public readonly string $name;

    #[Field(Address::class, isArray: true)]
    public readonly array $previousAddresses;
}

$user = new UserWithHistory([
    'name' => 'Charlie',
    'previousAddresses' => [
        ['city' => 'Paris', 'street' => 'Champs-Γ‰lysΓ©es'],
        ['city' => 'Rome', 'street' => 'Via del Corso'],
    ],
]);

Enums

enum UserType: string
{
    case Admin = 'admin';
    case Regular = 'regular';
    case Guest = 'guest';
}

final class Account extends Struct
{
    #[Field(UserType::class)]
    public readonly UserType $type;

    #[Field('string')]
    public readonly string $email;
}

$account = new Account([
    'type' => UserType::Admin,
    'email' => 'admin@example.com',
]);

JSON Serialization

$user = new User(['name' => 'Alice', 'address' => ['city' => 'Berlin', 'street' => 'Main St']]);

// To JSON
$json = $user->toJson(pretty: true);

// From JSON
$restored = User::fromJson($json);

// To Array
$array = $user->toArray(); // Recursive for nested structs

Cloning with Modifications

$user = new User(['name' => 'Alice', 'age' => 30]);

// Create modified copy
$updated = $user->with(['age' => 31]);

echo $user->age;    // 30 (original unchanged)
echo $updated->age; // 31 (new instance)

Strict Mode (Validate No Extra Fields)

use tommyknocker\struct\Struct;

// Enable strict mode globally
Struct::$strictMode = true;

final class ApiRequest extends Struct
{
    #[Field('string')]
    public readonly string $username;

    #[Field('string')]
    public readonly string $email;
}

// βœ… Valid - all fields are known
$request = new ApiRequest([
    'username' => 'john',
    'email' => 'john@example.com',
]);

// ❌ Throws RuntimeException: Unknown field: extra_field
$request = new ApiRequest([
    'username' => 'john',
    'email' => 'john@example.com',
    'extra_field' => 'not allowed!',
]);

// Disable strict mode (default behavior - extra fields ignored)
Struct::$strictMode = false;

Factory Pattern

use tommyknocker\struct\factory\StructFactory;

// Setup factory with dependencies
$factory = new StructFactory();

// Create struct instances
$user = $factory->create(User::class, [
    'name' => 'Alice',
    'email' => 'alice@example.com'
]);

// Create from JSON
$userFromJson = $factory->createFromJson(User::class, '{"name":"Bob","email":"bob@example.com"}');

Error Handling

use tommyknocker\struct\exception\ValidationException;
use tommyknocker\struct\exception\FieldNotFoundException;

try {
    $user = new User(['name' => 'John', 'email' => 'invalid-email']);
} catch (ValidationException $e) {
    echo "Validation error: " . $e->getMessage();
    echo "Field: " . $e->fieldName;
    echo "Value: " . $e->value;
} catch (FieldNotFoundException $e) {
    echo "Missing field: " . $e->getMessage();
}

Real-World API Example

// API endpoint for user registration
final class RegisterRequest extends Struct
{
    #[Field('string', validationRules: [new EmailRule()])]
    public readonly string $email;

    #[Field('string', validationRules: [new RangeRule(8, 50)])]
    public readonly string $password;

    #[Field('string', alias: 'full_name')]
    public readonly string $fullName;

    #[Field('int', nullable: true, validationRules: [new RangeRule(13, 120)])]
    public readonly ?int $age;
}

// In your API controller
public function register(Request $request): JsonResponse
{
    try {
        $data = RegisterRequest::fromJson($request->getContent());
        
        // Create user account
        $user = User::create([
            'email' => $data->email,
            'password' => Hash::make($data->password),
            'full_name' => $data->fullName,
            'age' => $data->age,
        ]);
        
        return response()->json([
            'success' => true,
            'user' => $user->toArray()
        ]);
        
    } catch (ValidationException $e) {
        return response()->json([
            'success' => false,
            'error' => $e->getMessage(),
            'field' => $e->fieldName
        ], 422);
    }
}

πŸ› οΈ Attribute Helper

The Attribute Helper automatically generates Field attributes for your Struct classes, reducing boilerplate code by up to 80% and ensuring consistent patterns across your codebase.

Why Use Attribute Helper?

  • βœ… Reduces boilerplate – No more manual attribute writing
  • βœ… Intelligent suggestions – Smart defaults based on property names and types
  • βœ… Consistent patterns – Ensures uniform attribute usage
  • βœ… Error prevention – Prevents typos and missing attributes
  • βœ… Rapid development – Generate attributes for entire projects in seconds

Console Usage

# Generate attributes for a single file
php scripts/struct-helper.php src/UserProfile.php

# Generate attributes for entire directory
php scripts/struct-helper.php src/

# Dry run (see what would be changed)
php scripts/struct-helper.php --dry-run src/

# Verbose output
php scripts/struct-helper.php --verbose src/

# Don't create backup files
php scripts/struct-helper.php --no-backup src/

Before and After

Before (Manual):

final class UserProfile extends Struct
{
    #[Field('string', validationRules: [new RequiredRule()], transformers: [new StringToUpperTransformer()])]
    public readonly string $firstName;

    #[Field('string', validationRules: [new RequiredRule()], transformers: [new StringToUpperTransformer()])]
    public readonly string $lastName;

    #[Field('string', validationRules: [new EmailRule()], transformers: [new StringToLowerTransformer()])]
    public readonly string $emailAddress;

    #[Field('string', nullable: true, alias: 'phone_number')]
    public readonly ?string $phoneNumber;

    #[Field('int', validationRules: [new RangeRule(13, 120)])]
    public readonly int $age;
}

After (Auto-generated):

final class UserProfile extends Struct
{
    public readonly string $firstName;
    public readonly string $lastName;
    public readonly string $emailAddress;
    public readonly ?string $phoneNumber;
    public readonly int $age;
}

Run the helper and it automatically generates:

#[Field('string', validationRules: [new RequiredRule()], transformers: [new StringToUpperTransformer()])]
public readonly string $firstName;

#[Field('string', validationRules: [new RequiredRule()], transformers: [new StringToUpperTransformer()])]
public readonly string $lastName;

#[Field('string', validationRules: [new EmailRule()], transformers: [new StringToLowerTransformer()])]
public readonly string $emailAddress;

#[Field('string', nullable: true, alias: 'phone_number')]
public readonly ?string $phoneNumber;

#[Field('int', validationRules: [new RangeRule(13, 120)])]
public readonly int $age;

Intelligent Features

Automatic Type Inference

public readonly string $name;        // β†’ #[Field('string')]
public readonly int $age;            // β†’ #[Field('int')]
public readonly ?string $email;      // β†’ #[Field('string', nullable: true)]
public readonly array $tags;         // β†’ #[Field('array', isArray: true)]
public readonly string|int $value;   // β†’ #[Field(['string', 'int'])]

Smart Validation Rules

public readonly string $email;       // β†’ validationRules: [new EmailRule()]
public readonly string $password;    // β†’ validationRules: [new RequiredRule()]
public readonly int $age;            // β†’ validationRules: [new RangeRule(1, 100)]
public readonly int $score;          // β†’ validationRules: [new RangeRule(1, 100)]

Automatic Field Aliases

public readonly string $firstName;   // β†’ alias: 'first_name'
public readonly string $emailAddress; // β†’ alias: 'email_address'
public readonly string $phoneNumber; // β†’ alias: 'phone_number'
public readonly string $createdAt;  // β†’ alias: 'created_at'

Smart Transformations

public readonly string $email;       // β†’ transformers: [new StringToLowerTransformer()]
public readonly string $username;    // β†’ transformers: [new StringToLowerTransformer()]
public readonly string $name;        // β†’ transformers: [new StringToUpperTransformer()]
public readonly string $title;      // β†’ transformers: [new StringToUpperTransformer()]

Intelligent Defaults

public readonly bool $isEnabled;     // β†’ default: true
public readonly bool $isActive;      // β†’ default: true
public readonly int $port;           // β†’ default: 3306
public readonly string $host;        // β†’ default: 'localhost'
public readonly array $items;        // β†’ default: []

Programmatic Usage

use tommyknocker\struct\tools\AttributeHelper;

$helper = new AttributeHelper();

// Generate attribute for a single property
$property = new ReflectionProperty(MyStruct::class, 'email');
$attribute = $helper->generateFieldAttribute($property);
echo $attribute; // #[Field('string', validationRules: [new EmailRule()], transformers: [new StringToLowerTransformer()])]

// Process entire class
$attributes = $helper->processClass(MyStruct::class);
foreach ($attributes as $propertyName => $attribute) {
    echo "{$propertyName}: {$attribute}\n";
}

// Get properties that need attributes
$properties = $helper->getPropertiesNeedingAttributes(MyStruct::class);
foreach ($properties as $property) {
    echo "Property {$property->getName()} needs an attribute\n";
}

Real-World Example

// API Integration Scenario
final class ProductApiResponse extends Struct
{
    public readonly string $productId;
    public readonly string $productName;
    public readonly float $price;
    public readonly ?string $description;
    public readonly array $categories;
    public readonly bool $isAvailable;
    public readonly string $createdAt;
    public readonly string $updatedAt;
}

// Run: php scripts/struct-helper.php ProductApiResponse.php
// Generates all necessary attributes automatically!

Error Handling

use tommyknocker\struct\tools\exception\AttributeHelperException;
use tommyknocker\struct\tools\exception\FileProcessingException;
use tommyknocker\struct\tools\exception\ClassProcessingException;

try {
    $helper = new AttributeHelper();
    $attributes = $helper->processClass('MyClass');
} catch (ClassProcessingException $e) {
    echo "Failed to process class: {$e->getMessage()}";
} catch (AttributeHelperException $e) {
    echo "Attribute generation failed: {$e->getMessage()}";
}

πŸ‘‰ See attribute helper examples for detailed demonstrations

πŸ’‘ Best Practices

1. Always Validate Input

// βœ… Good - Validate all incoming data
$userData = UserRequest::fromJson($request->getContent());

// ❌ Bad - Trusting raw input
$userData = json_decode($request->getContent(), true);

2. Use Specific Exception Types

try {
    $data = MyStruct::fromJson($json);
} catch (ValidationException $e) {
    // Handle validation errors specifically
    return response()->json(['error' => $e->getMessage()], 422);
} catch (FieldNotFoundException $e) {
    // Handle missing fields
    return response()->json(['error' => 'Missing required field'], 400);
}

3. Leverage Field Aliases for API Integration

final class ApiResponse extends Struct
{
    #[Field('string', alias: 'user_name')]
    public readonly string $userName;
    
    #[Field('string', alias: 'email_address')]
    public readonly string $emailAddress;
}

// Works with external API that uses snake_case
$response = new ApiResponse([
    'user_name' => 'John Doe',
    'email_address' => 'john@example.com'
]);

4. Use Default Values for Optional Fields

final class Config extends Struct
{
    #[Field('string', default: 'localhost')]
    public readonly string $host;
    
    #[Field('int', default: 3306)]
    public readonly int $port;
    
    #[Field('bool', default: false)]
    public readonly bool $debug;
}

// All fields get defaults if not provided
$config = new Config([]);

5. Combine Validation Rules for Complex Logic

final class PasswordField extends Struct
{
    #[Field('string', validationRules: [
        new RequiredRule(),
        new RangeRule(8, 128),
        new PasswordStrengthRule()
    ])]
    public readonly string $password;
}

❓ FAQ

Q: How is this different from regular PHP classes?

A: Struct provides automatic validation, type casting, JSON serialization, and immutability out of the box. Regular classes require manual implementation of these features.

Q: Can I use this with existing frameworks?

A: Yes! Struct works with any PHP framework. See the examples for Laravel, Symfony, and Slim integration.

Q: What about performance?

A: Struct uses reflection caching and optimized metadata systems. It's designed for production use with minimal overhead.

Q: Can I extend Struct classes?

A: Yes, but remember that Struct classes are immutable. Use the with() method to create modified copies.

Q: How do I handle optional fields?

A: Use nullable: true for fields that can be null, or default: value for fields with default values.

Q: What validation rules are available?

A: Built-in rules include EmailRule, RangeRule, RequiredRule. You can create custom rules by extending ValidationRule.

Q: Can I use this for database models?

A: Struct is designed for data validation and transfer, not ORM functionality. Use it for DTOs, API requests/responses, and data validation.

πŸ§ͺ Testing

The library is thoroughly tested with 100% code coverage:

composer test

All examples are verified to work:

composer test-examples

πŸ› οΈ Development

This project follows PSR-12 coding standards and uses PHPStan Level 9 for static analysis.

For contributors:

  • Run composer check to verify all tests and standards
  • Follow the existing code style
  • Add tests for new features
  • Update documentation as needed

πŸ“ API Reference

Field Attribute

#[Field(
    type: string|array<string>,                    // Type: 'string', 'int', 'float', 'bool', 'mixed', class-string, or array of types for union
    nullable: bool = false,                        // Allow null values
    isArray: bool = false,                         // Field is array of type
    default: mixed = null,                         // Default value if not provided
    alias: ?string = null,                         // Alternative key name in input data
    validator: ?string = null,                     // Legacy validator class with static validate() method
    validationRules: array = [],                   // Array of ValidationRule instances
    transformers: array = []                       // Array of TransformerInterface instances
)]

Validation Rules

// Built-in validation rules
use tommyknocker\struct\validation\rules\EmailRule;
use tommyknocker\struct\validation\rules\RangeRule;
use tommyknocker\struct\validation\rules\RequiredRule;

// Custom validation rule
class CustomRule extends \tommyknocker\struct\validation\ValidationRule
{
    public function validate(mixed $value): \tommyknocker\struct\validation\ValidationResult
    {
        // Your validation logic
        return \tommyknocker\struct\validation\ValidationResult::valid();
    }
}

Data Transformers

// Built-in transformers
use tommyknocker\struct\transformation\StringToUpperTransformer;
use tommyknocker\struct\transformation\StringToLowerTransformer;

// Custom transformer
class CustomTransformer implements \tommyknocker\struct\transformation\TransformerInterface
{
    public function transform(mixed $value): mixed
    {
        // Your transformation logic
        return $value;
    }
}

Factory and Serialization

// Factory for struct creation
use tommyknocker\struct\factory\StructFactory;

// JSON serialization
use tommyknocker\struct\serialization\JsonSerializer;

// Metadata system
use tommyknocker\struct\metadata\MetadataFactory;
use tommyknocker\struct\metadata\StructMetadata;
use tommyknocker\struct\metadata\FieldMetadata;

Struct Methods

// Constructor
public function __construct(array $data)

// Array conversion (recursive)
public function toArray(): array

// JSON serialization
public function toJson(bool $pretty = false, int $flags = ...): string

// Create from JSON
public static function fromJson(string $json, int $flags = JSON_THROW_ON_ERROR): static

// Clone with modifications
public function with(array $changes): static

// ArrayAccess (read-only)
public function offsetExists(mixed $offset): bool
public function offsetGet(mixed $offset): mixed

// JsonSerializable
public function jsonSerialize(): mixed

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Run tests and checks (composer check)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

  • Inspired by modern typed data structures in other languages
  • Built with modern PHP 8.1+ features
  • Tested with PHPUnit 11
  • Analyzed with PHPStan Level 9

πŸ“§ Author

Vasiliy Krivoplyas
Email: vasiliy@krivoplyas.com

πŸ”— Links