tommyknocker / struct
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
Requires
- php: ^8.1
- psr/container: ^2.0
Requires (Dev)
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2025-10-20 16:43:25 UTC
README
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.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Make your changes
- Run tests and checks (
composer check
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - 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