diego-ninja / granite
A lightweight zero-dependency PHP library for building immutable, serializable objects with validation capabilities.
Requires
- php: ^8.3|^8.4
Requires (Dev)
- fakerphp/faker: ^1.23
- mockery/mockery: ^1.6
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^11.0
- rector/rector: ^1.0
- squizlabs/php_codesniffer: ^3.7
This package is auto-updated.
Last update: 2025-06-06 10:10:35 UTC
README
A powerful, zero-dependency PHP library for building immutable, serializable objects with validation and mapping capabilities. Perfect for DTOs, Value Objects, API responses, and domain modeling.
This documentation has been generated almost in its entirety using ๐ฆ Claude 4 Sonnet based on source code analysis. Some sections may be incomplete, outdated or may contain documentation for planned or not-released features. For the most accurate information, please refer to the source code or open an issue on the package repository.
โจ Features
๐ Immutable Objects
- Read-only DTOs and Value Objects
- Thread-safe by design
- Functional programming friendly
โ Comprehensive Validation
- 25+ built-in validation rules
- Attribute-based validation (PHP 8+)
- Custom validation rules and callbacks
- Conditional and nested validation
๐ Powerful ObjectMapper
- Automatic property mapping between objects
- Convention-based mapping with multiple naming conventions
- Custom transformations and collection mapping
- Bidirectional mapping support
๐ฆ Smart Serialization
- JSON/Array serialization with custom property names
- Hide sensitive properties automatically
- DateTime and Enum handling
- Nested object serialization
โก Performance Optimized
- Reflection caching for improved performance
- Memory-efficient object creation
- Lazy loading support
๐ Quick Start
Installation
composer require diego-ninja/granite
Basic Usage
<?php use Ninja\Granite\GraniteVO; use Ninja\Granite\Validation\Attributes\Required; use Ninja\Granite\Validation\Attributes\Email; use Ninja\Granite\Validation\Attributes\Min; use Ninja\Granite\Serialization\Attributes\SerializedName; use Ninja\Granite\Serialization\Attributes\Hidden; // Create a Value Object with validation final readonly class User extends GraniteVO { public function __construct( public ?int $id, #[Required] #[Min(2)] public string $name, #[Required] #[Email] public string $email, #[Hidden] // Won't appear in JSON public ?string $password = null, #[SerializedName('created_at')] public DateTime $createdAt = new DateTime() ) {} } // Create and validate $user = User::from([ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'secret123' ]); // Immutable updates $updatedUser = $user->with(['name' => 'Jane Doe']); // Serialization $json = $user->json(); // {"id":null,"name":"John Doe","email":"john@example.com","created_at":"2024-01-15T10:30:00+00:00"} $array = $user->array(); // password is hidden, created_at uses custom name
๐ Documentation
Core Concepts
- Validation - Comprehensive validation system with 25+ built-in rules
- Serialization - Control how objects are converted to/from arrays and JSON
- ObjectMapper - Powerful object-to-object mapping with conventions
- Advanced Usage - Patterns for complex applications
- API Reference - Complete API documentation
Guides
- Migration Guide - Migrate from arrays, stdClass, Doctrine, Laravel
- Troubleshooting - Common issues and solutions
๐ฏ Use Cases
API Development
// Request validation final readonly class CreateUserRequest extends GraniteVO { public function __construct( #[Required] #[StringType] #[Min(2)] public string $name, #[Required] #[Email] public string $email, #[Required] #[Min(8)] #[Regex('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/', 'Password must contain uppercase, lowercase, and number')] public string $password ) {} } // API Response final readonly class UserResponse extends GraniteDTO { public function __construct( public int $id, public string $name, public string $email, #[SerializedName('member_since')] public DateTime $createdAt ) {} public static function fromEntity(User $user): self { return new self( id: $user->id, name: $user->name, email: $user->email, createdAt: $user->createdAt ); } }
Domain Modeling
// Value Objects final readonly class Money extends GraniteVO { public function __construct( #[Required] #[NumberType] #[Min(0)] public float $amount, #[Required] #[StringType] #[Min(3)] #[Max(3)] public string $currency ) {} public function add(Money $other): Money { if ($this->currency !== $other->currency) { throw new InvalidArgumentException('Cannot add different currencies'); } return new Money($this->amount + $other->amount, $this->currency); } } // Aggregates final readonly class Order extends GraniteVO { public function __construct( public ?int $id, #[Required] public int $customerId, #[Required] #[ArrayType] #[Each(new Rules\Callback(fn($item) => OrderItem::from($item)))] public array $items, #[Required] public OrderStatus $status = OrderStatus::PENDING ) {} public function getTotal(): Money { $total = new Money(0.0, 'USD'); foreach ($this->items as $item) { $total = $total->add($item->getTotal()); } return $total; } }
Object Mapping with ObjectMapper
use Ninja\Granite\Mapping\ObjectMapper; use Ninja\Granite\Mapping\ObjectMapperConfig; use Ninja\Granite\Mapping\Attributes\MapFrom; // Source entity final readonly class UserEntity extends GraniteDTO { public function __construct( public int $userId, public string $fullName, public string $emailAddress, public DateTime $createdAt ) {} } // Destination DTO with mapping attributes final readonly class UserSummary extends GraniteDTO { public function __construct( #[MapFrom('userId')] public int $id, #[MapFrom('fullName')] public string $name, #[MapFrom('emailAddress')] public string $email ) {} } // Create ObjectMapper with clean configuration $mapper = new ObjectMapper( ObjectMapperConfig::forProduction() ->withConventions(true, 0.8) ->withSharedCache() ); // Automatic mapping $summary = $mapper->map($userEntity, UserSummary::class);
๐ฅ Advanced Features
Convention-Based Mapping
// Automatically maps between different naming conventions class SourceClass { public string $firstName; // camelCase public string $email_address; // snake_case public string $UserID; // PascalCase } class DestinationClass { public string $first_name; // snake_case public string $emailAddress; // camelCase public string $user_id; // snake_case } $mapper = new ObjectMapper( ObjectMapperConfig::create() ->withConventions(true, 0.8) ); $result = $mapper->map($source, DestinationClass::class); // Properties automatically mapped based on naming conventions!
Advanced ObjectMapper Configuration
use Ninja\Granite\Mapping\ObjectMapperConfig; use Ninja\Granite\Mapping\MappingProfile; // Fluent configuration with builder pattern $mapper = new ObjectMapper( ObjectMapperConfig::forProduction() ->withSharedCache() ->withConventions(true, 0.8) ->withProfile(new UserMappingProfile()) ->withProfile(new ProductMappingProfile()) ->withWarmup() ); // Predefined configurations $devMapper = new ObjectMapper(ObjectMapperConfig::forDevelopment()); $prodMapper = new ObjectMapper(ObjectMapperConfig::forProduction()); $testMapper = new ObjectMapper(ObjectMapperConfig::forTesting());
Custom Mapping Profiles
use Ninja\Granite\Mapping\MappingProfile; class UserMappingProfile extends MappingProfile { protected function configure(): void { // Complex transformations $this->createMap(UserEntity::class, UserResponse::class) ->forMember('fullName', fn($m) => $m->using(function($value, $sourceData) { return $sourceData['firstName'] . ' ' . $sourceData['lastName']; }) ) ->forMember('age', fn($m) => $m->mapFrom('birthDate') ->using(fn($birthDate) => (new DateTime())->diff($birthDate)->y) ) ->forMember('isActive', fn($m) => $m->mapFrom('status') ->using(fn($status) => $status === 'active') ) ->seal(); // Bidirectional mapping $this->createMapBidirectional(UserEntity::class, UserDto::class) ->forMembers('userId', 'id') ->forMembers('fullName', 'name') ->forMembers('emailAddress', 'email') ->seal(); } }
Complex Validation
final readonly class CreditCard extends GraniteVO { public function __construct( #[Required] #[Regex('/^\d{4}\s?\d{4}\s?\d{4}\s?\d{4}$/', 'Invalid card number format')] public string $number, #[Required] #[Regex('/^(0[1-9]|1[0-2])\/([0-9]{2})$/', 'Invalid expiry format (MM/YY)')] public string $expiry, #[Required] #[Regex('/^\d{3,4}$/', 'Invalid CVV')] public string $cvv, #[When( condition: fn($value, $data) => $data['type'] === 'business', rule: new Required() )] public ?string $companyName = null ) {} protected static function rules(): array { return [ 'number' => [ new Callback(function($number) { // Luhn algorithm validation return $this->isValidLuhn(str_replace(' ', '', $number)); }, 'Invalid credit card number') ] ]; } }
Global ObjectMapper Configuration
// Configure once at application startup ObjectMapper::configure(function(ObjectMapperConfig $config) { $config->withSharedCache() ->withConventions(true, 0.8) ->withProfiles([ new UserMappingProfile(), new ProductMappingProfile(), new OrderMappingProfile() ]) ->withWarmup(); }); // Use anywhere in your application $mapper = ObjectMapper::getInstance(); $userDto = $mapper->map($userEntity, UserDto::class);
๐ Validation Rules
Built-in Rules
Rule | Description | Example |
---|---|---|
#[Required] |
Field must not be null | #[Required('Name is required')] |
#[Email] |
Valid email format | #[Email('Invalid email')] |
#[Min(5)] |
Minimum length/value | #[Min(5, 'Too short')] |
#[Max(100)] |
Maximum length/value | #[Max(100, 'Too long')] |
#[Regex('/pattern/')] |
Regular expression | #[Regex('/^\d+$/', 'Numbers only')] |
#[In(['a', 'b'])] |
Value in list | #[In(['active', 'inactive'])] |
#[Url] |
Valid URL format | #[Url('Invalid URL')] |
#[IpAddress] |
Valid IP address | #[IpAddress('Invalid IP')] |
#[StringType] |
Must be string | #[StringType] |
#[IntegerType] |
Must be integer | #[IntegerType] |
#[NumberType] |
Must be number | #[NumberType] |
#[BooleanType] |
Must be boolean | #[BooleanType] |
#[ArrayType] |
Must be array | #[ArrayType] |
#[EnumType] |
Valid enum value | #[EnumType(Status::class)] |
#[Each(...)] |
Validate array items | #[Each(new Email())] |
#[When(...)] |
Conditional validation | #[When($condition, $rule)] |
๐ Performance
Granite is optimized for performance with:
- Reflection caching - Class metadata cached automatically
- Mapping cache - ObjectMapper configurations cached
- Memory efficiency - Immutable objects reduce memory overhead
- Lazy loading - Load related data only when needed
- Specialized components - Refactored architecture with focused responsibilities
// Use shared cache for web applications $mapper = new ObjectMapper( ObjectMapperConfig::forProduction() ->withSharedCache() ->withWarmup() // Preload configurations ); // Preload mappings for better performance use Ninja\Granite\Mapping\MappingPreloader; MappingPreloader::preload($mapper, [ [UserEntity::class, UserResponse::class], [ProductEntity::class, ProductResponse::class] ]);
๐งช Testing
Granite objects are perfect for testing due to their immutability and validation:
class UserTest extends PHPUnit\Framework\TestCase { public function testUserCreation(): void { $user = User::from([ 'name' => 'John Doe', 'email' => 'john@example.com' ]); $this->assertEquals('John Doe', $user->name); $this->assertEquals('john@example.com', $user->email); } public function testObjectMapping(): void { $mapper = new ObjectMapper(ObjectMapperConfig::forTesting()); $entity = new UserEntity(1, 'John Doe', 'john@example.com', new DateTime()); $dto = $mapper->map($entity, UserDto::class); $this->assertInstanceOf(UserDto::class, $dto); $this->assertEquals(1, $dto->id); } public function testImmutability(): void { $user = User::from(['name' => 'John', 'email' => 'john@example.com']); $updated = $user->with(['name' => 'Jane']); // Original unchanged $this->assertEquals('John', $user->name); // New instance created $this->assertEquals('Jane', $updated->name); } }
๐ง Requirements
- PHP 8.3+ - Takes advantage of modern PHP features
- No dependencies - Zero external dependencies for maximum compatibility
๐ฆ Installation & Setup
# Install via Composer composer require diego-ninja/granite # Optional: Configure cache directory for persistent mapping cache mkdir cache/granite chmod 755 cache/granite
๐ค Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
๐ License
This package is open-sourced software licensed under the MIT license.
๐ Credits
This project is developed and maintained by ๐ฅท Diego Rin in his free time.
If you find this project useful, please consider:
- โญ Starring the repository
- ๐ Reporting bugs and issues
- ๐ก Suggesting new features
- ๐ง Contributing code improvements
Made with โค๏ธ for the PHP community