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

v1.0.0 2025-10-09 07:28 UTC

This package is auto-updated.

Last update: 2025-10-20 16:43:25 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
  • βœ… Type validation – Scalars, objects, arrays, enums, DateTime
  • πŸ”’ 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
  • βœ”οΈ Custom validators – Add your own validation logic
  • 🎭 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
  • πŸ” PHPStan Level 9 – Maximum static analysis
  • πŸ§ͺ 100% tested – PHPUnit coverage
  • ⚑ Performance optimized – Reflection caching

🎯 Use Cases

Perfect for:

  • πŸ“± REST API validation for mobile apps
  • πŸ”„ Data Transfer Objects (DTOs) in clean architecture
  • 🌐 Third-party API integration with field mapping
  • βœ… Form validation with complex rules
  • πŸ“Š Data serialization/deserialization
  • πŸ›‘οΈ Type-safe data handling in microservices

πŸ‘‰ 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'
]);

Custom Validation

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 RuntimeException

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;

PSR-11 Container Integration

use Psr\Container\ContainerInterface;
use tommyknocker\struct\Struct;

// Setup container
$container = new SimpleContainer();
Struct::$container = $container;

// Register Address
$container->set(Address::class, new Address(['city' => 'Amsterdam', 'street' => 'Damrak']));

// Create User - Address will be resolved from container
$user = new User([
    'name' => 'Alice',
    'address' => ['city' => 'Amsterdam', 'street' => 'Damrak'],
]);

πŸ§ͺ Testing

Run the test suite:

composer test

Run PHPStan static analysis:

composer phpstan

Check code style:

composer cs-check

Run all checks:

composer check

πŸ› οΈ Development

Code Style

This project uses PHP-CS-Fixer with PSR-12 standard:

composer cs-fix

Static Analysis

PHPStan is configured at level 9 for maximum type safety:

composer phpstan

πŸ“ API Reference

Field Attribute

#[Field(
    type: string,              // Type: 'string', 'int', 'float', 'bool', 'mixed', or class-string
    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  // Validator class with static validate() method
)]

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