hypothesisphp / lens
Lens is a reusable object that knows how to locate, read, and transform a specific piece of data inside any object or array.
v1.0.0
2026-07-17 08:54 UTC
Requires
- php: >=8.2
- ext-mbstring: *
Requires (Dev)
- infection/infection: ^0.29
- phpbench/phpbench: ^1.3
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
- vimeo/psalm: ^6.0
This package is not auto-updated.
Last update: 2026-07-18 07:11:33 UTC
README
Lens is a reusable object that knows how to locate, read, and transform a specific piece of data inside any object or array.
Installation
composer require hypothesisphp/lens
Quick Start
use Lens; // Get a nested value $city = Lens::path('address.city')->get($user); // Set (immutable — returns a new copy) $updated = Lens::path('address.city')->set($user, 'Bandung'); // Update (transform in place) $updated = Lens::path('price')->update($product, fn($v) => $v * 1.1); // Batch (single clone, multiple changes) $updated = Lens::batch($user) ->set('profile.name', 'John') ->update('profile.age', fn($v) => $v + 1) ->set('address.city', 'Bandung') ->apply();
Why Lens?
The Problem
Working with nested data in PHP is verbose and error-prone:
// Without Lens $clone = clone $user; $clone->address = clone $user->address; $clone->address->city = 'Bandung'; $clone->profile = clone $user->profile; $clone->profile->name = 'John'; $clone->profile->age = $user->profile->age + 1;
The Solution
// With Lens $clone = Lens::batch($user) ->set('address.city', 'Bandung') ->set('profile.name', 'John') ->update('profile.age', fn($v) => $v + 1) ->apply(); // ONE clone, all changes applied
Features
Read
// Simple property Lens::path('name')->get($user); // 'Alice' // Deeply nested Lens::path('orders.0.items.2.price')->get($user); // 25.0 // Wildcard (all elements) Lens::path('orders.*.price')->get($user); // [100, 200, 300] // Nested wildcards Lens::path('orders.*.items.*.price')->get($user); // [[10, 20], [30]]
Write (Immutable)
// Returns a NEW object — original unchanged $updated = Lens::path('name')->set($user, 'Bob'); // $user->name is still 'Alice' // $updated->name is 'Bob' // Deep set $updated = Lens::path('address.city')->set($user, 'Bandung');
Transform
// Update with a callable $updated = Lens::path('age')->update($user, fn($v) => $v + 1); // Update deeply nested $updated = Lens::path('address.zip')->update($user, fn($v) => strtoupper($v));
Mutable Mode
// Modifies the original object in place Lens::path('name')->mutate($user, 'Bob'); // $user->name is now 'Bob'
Batch Operations
// Multiple changes, single clone $updated = Lens::batch($user) ->set('name', 'Bob') ->set('age', 25) ->update('address.zip', fn($v) => $v . '-001') ->apply(); // Mutable batch (objects only) Lens::batch($user) ->set('name', 'Bob') ->applyAs('mutable');
Nullable & Default
// Returns null instead of throwing Lens::path('address.city')->nullable()->get($userWithoutAddress); // null // Returns default value Lens::path('address.city')->default('Unknown')->get($userWithoutAddress); // 'Unknown'
Composition
$addressLens = Lens::path('address'); $cityLens = Lens::path('city'); $addressCityLens = $addressLens->compose($cityLens); $city = $addressCityLens->get($user);
Pipelines
Lens::pipeline() ->pipe(fn($v) => strtoupper($v)) ->pipe(fn($v) => trim($v)) ->through(Lens::path('name')) ->get($user); // 'ALICE'
Path Syntax
| Syntax | Example | Meaning |
|---|---|---|
| Property | name |
Access name property/key |
| Dotted | address.city |
Deep access |
| Index | orders.0 |
Array index |
| Wildcard | orders.* |
All elements |
| Quoted | "special.key" |
Key with dots |
| Escaped | foo\.bar |
Literal dot in key |
Exists & Unset
Lens::path('name')->exists($user); // true/false // Remove a key (immutable) $updated = Lens::path('name')->unset($data);
Works With
- Objects — public, protected, private properties
- Arrays — associative and indexed
- Mixed — objects containing arrays containing objects
- ArrayAccess —
ArrayObject, custom implementations - DTOs — readonly properties
- Value Objects — immutable objects
Documentation
| Topic | Description |
|---|---|
| Getting Started | Install and first steps |
| Path Syntax | All path expressions |
| Immutable vs Mutable | When to use each mode |
| Batch Operations | Single-clone batch changes |
| Composition | Combining lenses |
| Traversals | Focusing on multiple targets |
| Pipelines | Chaining transformations |
| Attributes | PHP 8 Attribute configuration |
| Extending | Custom strategies, plugins, macros |
| Performance | Optimization details |
| Static Analysis | PHPStan & Psalm |
| Migration Guide | Moving from other approaches |
| Architecture | Complete design document |
Extensibility
Custom Clone Strategy
use Lens\Contracts\CloneStrategyInterface; class MyCloneStrategy implements CloneStrategyInterface { public function clone(mixed $data): mixed { /* ... */ } public function supports(mixed $data): bool { /* ... */ } }
Custom Metadata Provider
use Lens\Contracts\MetadataProviderInterface; class DoctrineMetadataProvider implements MetadataProviderInterface { // Read types from Doctrine metadata instead of reflection }
Macros
Lens::macro('shout', function (string $value): string { return strtoupper($value) . '!'; });
Plugins & Middleware
use Lens\Contracts\PluginInterface; use Lens\Contracts\MiddlewareInterface; // Log every lens operation class AuditPlugin implements PluginInterface { /* ... */ } // Cache lens reads class CacheMiddleware implements MiddlewareInterface { /* ... */ }
Static Analysis
Fully compatible with PHPStan (level max) and Psalm (level 1). Generic type annotations:
/** @var LensInterface<User, string> */ $lens = Lens::path('name');
Requirements
- PHP 8.2+
ext-mbstring
Architecture
The library is modular and interface-driven. Every component is independently replaceable:
Contracts → Operations → Navigation → Access → Strategy → Infrastructure
See ARCHITECTURE.md for the complete design document.
Testing
composer test # All tests composer test:unit # Unit tests only composer test:integration # Integration tests composer test:feature # Feature tests composer analyse # PHPStan composer psalm # Psalm composer mutate # Infection (mutation testing)
License
MIT License. See LICENSE.
Credits
Inspired by functional programming lenses (Haskell, Scala, Monocle) — redesigned for PHP.