A lightweight, type-safe structure helper for PHP 8.1+.
Define your data models with attributes, get automatic validation, array access, and JSON serialization.
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
Install via Composer:
composer require tommyknocker/structRequirements:
- PHP 8.1 or higher
- Composer
- π·οΈ 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
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
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)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]);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; // 3306final 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'
]);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 alloweduse 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
]); // β
Validuse 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 DOEclass 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 ValidationExceptionfinal 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'
]);final class Payload extends Struct
{
#[Field('string')]
public readonly string $type;
#[Field('mixed')]
public readonly mixed $data; // Can be anything
}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'],
]);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'],
],
]);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',
]);$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$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)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;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"}');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();
}// 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);
}
}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.
- β 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
# 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 (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;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'])]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)]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'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()]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: []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";
}// 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!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
// β
Good - Validate all incoming data
$userData = UserRequest::fromJson($request->getContent());
// β Bad - Trusting raw input
$userData = json_decode($request->getContent(), true);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);
}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'
]);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([]);final class PasswordField extends Struct
{
#[Field('string', validationRules: [
new RequiredRule(),
new RangeRule(8, 128),
new PasswordStrengthRule()
])]
public readonly string $password;
}A: Struct provides automatic validation, type casting, JSON serialization, and immutability out of the box. Regular classes require manual implementation of these features.
A: Yes! Struct works with any PHP framework. See the examples for Laravel, Symfony, and Slim integration.
A: Struct uses reflection caching and optimized metadata systems. It's designed for production use with minimal overhead.
A: Yes, but remember that Struct classes are immutable. Use the with() method to create modified copies.
A: Use nullable: true for fields that can be null, or default: value for fields with default values.
A: Built-in rules include EmailRule, RangeRule, RequiredRule. You can create custom rules by extending ValidationRule.
A: Struct is designed for data validation and transfer, not ORM functionality. Use it for DTOs, API requests/responses, and data validation.
The library is thoroughly tested with 100% code coverage:
composer testAll examples are verified to work:
composer test-examplesThis 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
#[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
)]// 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();
}
}// 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 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;// 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(): mixedContributions 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
This project is licensed under the MIT License - see the LICENSE file for details.
- 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
Vasiliy Krivoplyas
Email: vasiliy@krivoplyas.com