PHPackages                             hypothesisphp/lens - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. hypothesisphp/lens

ActiveLibrary

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(1mo ago)11↑2900%MITPHP &gt;=8.2

Since Jul 17Compare

[ Source](https://github.com/HypothesisPHP/Lens)[ Packagist](https://packagist.org/packages/hypothesisphp/lens)[ Docs](https://github.com/HypothesisPHP/Lens)[ RSS](/packages/hypothesisphp-lens/feed)WikiDiscussions Synced 1mo ago

READMEChangelogDependencies (5)Versions (2)Used By (0)

Lens
====

[](#lens)

> **Lens is a reusable object that knows how to locate, read, and transform a specific piece of data inside any object or array.**

---

Installation
------------

[](#installation)

```
composer require hypothesisphp/lens
```

Quick Start
-----------

[](#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?
---------

[](#why-lens)

### The Problem

[](#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

[](#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
--------

[](#features)

### Read

[](#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)

[](#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

[](#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

[](#mutable-mode)

```
// Modifies the original object in place
Lens::path('name')->mutate($user, 'Bob');
// $user->name is now 'Bob'
```

### Batch Operations

[](#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 &amp; Default

[](#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

[](#composition)

```
$addressLens = Lens::path('address');
$cityLens = Lens::path('city');
$addressCityLens = $addressLens->compose($cityLens);

$city = $addressCityLens->get($user);
```

### Pipelines

[](#pipelines)

```
Lens::pipeline()
    ->pipe(fn($v) => strtoupper($v))
    ->pipe(fn($v) => trim($v))
    ->through(Lens::path('name'))
    ->get($user); // 'ALICE'
```

### Path Syntax

[](#path-syntax)

SyntaxExampleMeaningProperty`name`Access `name` property/keyDotted`address.city`Deep accessIndex`orders.0`Array indexWildcard`orders.*`All elementsQuoted`"special.key"`Key with dotsEscaped`foo\.bar`Literal dot in key### Exists &amp; Unset

[](#exists--unset)

```
Lens::path('name')->exists($user); // true/false

// Remove a key (immutable)
$updated = Lens::path('name')->unset($data);
```

Works With
----------

[](#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
-------------

[](#documentation)

TopicDescription[Getting Started](docs/getting-started.md)Install and first steps[Path Syntax](docs/path-syntax.md)All path expressions[Immutable vs Mutable](docs/immutable-vs-mutable.md)When to use each mode[Batch Operations](docs/batch-operations.md)Single-clone batch changes[Composition](docs/composition.md)Combining lenses[Traversals](docs/traversals.md)Focusing on multiple targets[Pipelines](docs/pipelines.md)Chaining transformations[Attributes](docs/attributes.md)PHP 8 Attribute configuration[Extending](docs/extending.md)Custom strategies, plugins, macros[Performance](docs/performance.md)Optimization details[Static Analysis](docs/static-analysis.md)PHPStan &amp; Psalm[Migration Guide](docs/migration-guide.md)Moving from other approaches[Architecture](ARCHITECTURE.md)Complete design documentExtensibility
-------------

[](#extensibility)

### Custom Clone Strategy

[](#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

[](#custom-metadata-provider)

```
use Lens\Contracts\MetadataProviderInterface;

class DoctrineMetadataProvider implements MetadataProviderInterface
{
    // Read types from Doctrine metadata instead of reflection
}
```

### Macros

[](#macros)

```
Lens::macro('shout', function (string $value): string {
    return strtoupper($value) . '!';
});
```

### Plugins &amp; Middleware

[](#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
---------------

[](#static-analysis)

Fully compatible with PHPStan (level max) and Psalm (level 1). Generic type annotations:

```
/** @var LensInterface */
$lens = Lens::path('name');
```

Requirements
------------

[](#requirements)

- PHP 8.2+
- `ext-mbstring`

Architecture
------------

[](#architecture)

The library is modular and interface-driven. Every component is independently replaceable:

```
Contracts → Operations → Navigation → Access → Strategy → Infrastructure

```

See [ARCHITECTURE.md](ARCHITECTURE.md) for the complete design document.

Testing
-------

[](#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
-------

[](#license)

MIT License. See [LICENSE](LICENSE).

Credits
-------

[](#credits)

Inspired by functional programming lenses (Haskell, Scala, Monocle) — redesigned for PHP.

###  Health Score

38

—

LowBetter than 82% of packages

Maintenance90

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

46d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/155282015?v=4)[Fitri HY](/maintainers/fitri-hy)[@fitri-hy](https://github.com/fitri-hy)

---

Tags

phpfunctionaldtoimmutabletraversallensdata accessoptic

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Psalm

Type Coverage Yes

### Embed Badge

![Health badge](/badges/hypothesisphp-lens/health.svg)

```
[![Health](https://phpackages.com/badges/hypothesisphp-lens/health.svg)](https://phpackages.com/packages/hypothesisphp-lens)
```

###  Alternatives

[mpetrovich/dash

A functional programming library for PHP. Inspired by Underscore, Lodash, and Ramda.

10430.7k1](/packages/mpetrovich-dash)[transprime-research/piper

PHP Pipe method execution with values from chained method executions

174.7k2](/packages/transprime-research-piper)[fab2s/dt0

Immutable DTOs with bidirectional casting. No framework required. 8x faster than the alternative.

102.8k2](/packages/fab2s-dt0)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
