Lens is a reusable object that knows how to locate, read, and transform a specific piece of data inside any object or array.
composer require hypothesisphp/lensuse 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();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;// 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// 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]]// 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');// 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));// Modifies the original object in place
Lens::path('name')->mutate($user, 'Bob');
// $user->name is now 'Bob'// 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');// 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'$addressLens = Lens::path('address');
$cityLens = Lens::path('city');
$addressCityLens = $addressLens->compose($cityLens);
$city = $addressCityLens->get($user);Lens::pipeline()
->pipe(fn($v) => strtoupper($v))
->pipe(fn($v) => trim($v))
->through(Lens::path('name'))
->get($user); // 'ALICE'| 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 |
Lens::path('name')->exists($user); // true/false
// Remove a key (immutable)
$updated = Lens::path('name')->unset($data);- 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
| 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 |
use Lens\Contracts\CloneStrategyInterface;
class MyCloneStrategy implements CloneStrategyInterface
{
public function clone(mixed $data): mixed { /* ... */ }
public function supports(mixed $data): bool { /* ... */ }
}use Lens\Contracts\MetadataProviderInterface;
class DoctrineMetadataProvider implements MetadataProviderInterface
{
// Read types from Doctrine metadata instead of reflection
}Lens::macro('shout', function (string $value): string {
return strtoupper($value) . '!';
});use Lens\Contracts\PluginInterface;
use Lens\Contracts\MiddlewareInterface;
// Log every lens operation
class AuditPlugin implements PluginInterface { /* ... */ }
// Cache lens reads
class CacheMiddleware implements MiddlewareInterface { /* ... */ }Fully compatible with PHPStan (level max) and Psalm (level 1). Generic type annotations:
/** @var LensInterface<User, string> */
$lens = Lens::path('name');- PHP 8.2+
ext-mbstring
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.
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)MIT License. See LICENSE.
Inspired by functional programming lenses (Haskell, Scala, Monocle) — redesigned for PHP.