diego-ninja/granite

A lightweight zero-dependency PHP library for building immutable, serializable objects with validation capabilities.

Maintainers

Package info

github.com/diego-ninja/granite

pkg:composer/diego-ninja/granite

Statistics

Installs: 968

Dependents: 1

Suggesters: 0

Stars: 59

Open Issues: 0


README

Latest Version on Packagist Total Downloads PHP Version License: MIT GitHub last commit wakatime

Tests Static Analysis Code Style Coveralls

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.

๐Ÿชถ Pebble - Lightweight Immutable Snapshots

Pebble is a lightweight alternative to Granite for when you need immutable snapshots without validation overhead โ€” ideal for caching, Eloquent snapshots, and fast comparisons.

$snapshot = Pebble::from($eloquentModel);

$snapshot->name;           // Magic __get
$snapshot['email'];        // ArrayAccess
$snapshot->equals($other); // O(1) fingerprint comparison

๐Ÿ“– Read Pebble Documentation

โœจ Features

  • Immutable Objects โ€” Read-only DTOs and Value Objects, thread-safe by design
  • Flexible from() Method โ€” Create from arrays, JSON, named parameters, other Granite objects, or mix them
  • Comprehensive Validation โ€” 30+ built-in rules including Carbon date validation (Age, Future, Past, BusinessDay...)
  • ObjectMapper โ€” Convention-based property mapping between objects with custom transformations
  • Smart Serialization โ€” Custom property names, naming conventions, hidden fields, Carbon date formats
  • Object Comparison โ€” Deep equality with equals(), detailed diffs with differs()
  • Performance Optimized โ€” Fast path for simple DTOs (~3.5x vs plain PHP), WeakMap caching, direct property access

๐Ÿš€ Quick Start

Installation

composer require diego-ninja/granite

Basic Usage

use Ninja\Granite\Granite;
use Ninja\Granite\Validation\Attributes\Required;
use Ninja\Granite\Validation\Attributes\Email;
use Ninja\Granite\Validation\Attributes\Min;
use Ninja\Granite\Serialization\Attributes\Hidden;

final readonly class User extends Granite
{
    public function __construct(
        #[Required]
        #[Min(2)]
        public string $name,

        #[Required]
        #[Email]
        public string $email,

        #[Hidden]
        public ?string $password = null,
    ) {}
}

// Create from array
$user = User::from(['name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'secret']);

// Create from named parameters
$user = User::from(name: 'John Doe', email: 'john@example.com');

// Immutable updates
$updated = $user->with(['name' => 'Jane Doe']);

// Serialization (password hidden automatically)
$json = $user->json();   // {"name":"John Doe","email":"john@example.com"}
$array = $user->array();

๐Ÿ“– Documentation

Core Concepts

  • Enhanced from() Method โ€” Multiple invocation patterns for flexible object creation
  • Validation โ€” Comprehensive validation system with 30+ built-in rules including Carbon
  • Serialization โ€” Control how objects are converted to/from arrays and JSON with Carbon support
  • Object Comparison โ€” Deep equality checks and difference detection
  • ObjectMapper โ€” Powerful object-to-object mapping with conventions
  • Pebble โ€” Lightweight immutable snapshots with fingerprinting
  • Advanced Usage โ€” Patterns for complex applications
  • API Reference โ€” Complete API documentation

Guides

๐Ÿ“ˆ Performance & Benchmarks

Granite uses a multi-layer fast path system that detects simple DTOs at class-load time and bypasses the full hydration pipeline (reflection, metadata, type conversion) entirely. For objects that qualify, the overhead vs plain PHP constructors is minimal.

Benchmark Results

Benchmarked on PHP 8.4, comparing Granite against plain PHP constructors and Pebble (Granite's lightweight companion). The test DTO has 6 fields (int, string, string, int, string, bool).

Object Creation

Benchmark ยตs/op vs Plain PHP
Plain PHP constructor 0.33 โ€”
Granite::from(array) 1.14 3.5x
Granite::from(named args) 1.13 3.4x
Pebble::from(array) 3.62 11.1x

Nested Object Creation (3 objects)

Benchmark ยตs/op vs Plain PHP
Plain PHP constructors 0.78 โ€”
Granite::from(array) 3.63 4.6x
Pebble::from(array) 6.80 8.7x

Granite recursively applies the fast path to nested Granite-typed properties, so the overhead scales linearly with object depth rather than exploding through the full hydration pipeline.

Serialization

Benchmark ยตs/op vs Plain PHP
Plain PHP toArray() 0.19 โ€”
Granite array() 0.25 1.3x
Plain PHP json_encode(array) 0.25 โ€”
Granite json() 0.26 1.0x

Both array() and json() results are cached in a WeakMap. Since Granite objects are readonly, the serialized representation never changes, so repeated calls return instantly. The json() method effectively matches native json_encode performance.

Equality Check

Benchmark ยตs/op vs Plain PHP
Plain array === 0.12 โ€”
Granite equals() 0.48 4.0x
Pebble equals() (fingerprint) 0.31 2.6x

For simple DTOs, equals() compares properties directly without building intermediate arrays, with early exit on the first difference.

Collection (100 items, create from array)

Benchmark ยตs/op vs Plain PHP
Plain PHP array_map + constructors 31 โ€”
Granite array_map + from() 106 3.4x
Pebble array_map + from() 343 11.1x

Property Access

Granite uses native PHP readonly promoted properties โ€” property access is identical to plain PHP objects, with zero overhead:

Benchmark ยตs/op
Plain PHP readonly 0.15
Granite readonly 0.14
Pebble __get() 1.03

How it works

Granite's performance comes from three layers of optimization:

  1. Fast path detection (ClassProfile) โ€” At class-load time, Granite analyzes each class and determines if it can skip the full hydration pipeline. A class qualifies when all constructor parameters are either primitive types (int, string, float, bool, array) or other Granite subclasses, and the class has no special attributes (#[Hidden], #[SerializedName], validation rules, etc.).

  2. WeakMap caching โ€” array() and json() results are cached in a WeakMap keyed by object instance. Since Granite objects are readonly, the cache is always valid. When the object is garbage collected, the cache entry is automatically cleaned up.

  3. Direct property access โ€” For serialization and comparison, Granite reads properties directly by name ($instance->$name) instead of going through reflection, metadata lookups, and type conversion.

Classes that don't qualify for the fast path (those with validation attributes, naming conventions, Carbon dates, etc.) use the standard hydration pipeline, which is still optimized with reflection caching and O(1) hidden property lookups.

Running the Benchmarks

php benchmarks/GraniteBench.php

โš ๏ธ Deprecation Notice

GraniteDTO and GraniteVO are deprecated since v2.0.0 in favor of the unified Granite base class. Both still extend Granite for backward compatibility but will be removed in v3.0.0.

// โŒ Deprecated โ€” use Granite instead
final readonly class User extends GraniteVO { }
// โœ…
final readonly class User extends Granite { }

๐Ÿ”ง Requirements

  • PHP 8.3+ โ€” Takes advantage of modern PHP features
  • No dependencies โ€” Zero external dependencies for maximum compatibility

๐Ÿ“ฆ Installation

composer require diego-ninja/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