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)
- friendsofphp/php-cs-fixer: ^3.89
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.0|^11.0
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
- β 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 checkto 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.
- 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