fuzzyfox/fluent-value

Fluent value objects with deep nesting and lazy evaluation support

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/fuzzyfox/fluent-value

v1.0.0 2026-01-30 13:40 UTC

This package is auto-updated.

Last update: 2026-01-30 13:54:13 UTC


README

A PHP package for creating fluent value objects that wrap arrays or preserve objects, providing both object syntax and ArrayAccess syntax with deep nesting and lazy evaluation support.

Features

  • Dual Syntax: Access data using both object ($data->key) and array ($data['key']) syntax
  • 🪆 Deep Nesting: Automatically wraps nested arrays; objects are preserved and accessible
  • Lazy Evaluation: Closures are resolved only when accessed, receiving parent context
  • 🔍 Dot Notation: Get, set, and check values using dot notation (user.profile.name)
  • 🛠️ Collection Methods: Includes map, filter, only, except, and more
  • 📦 Serializable: Implements JsonSerializable, Countable, and IteratorAggregate

Installation

composer require fuzzyfox/fluent-value

Basic Usage

Creating a FluentValue

use FuzzyFox\FluentValue;

// From array
$data = new FluentValue([
    'name' => 'John Doe',
    'email' => 'john@example.com',
    'age' => 30
]);

// Using static make method
$data = FluentValue::make(['name' => 'Jane']);

// Using helper function (if autoloaded)
$data = fluent(['name' => 'Bob']);

Accessing Data

$data = new FluentValue([
    'name' => 'Alice',
    'email' => 'alice@example.com'
]);

// Object syntax
echo $data->name; // "Alice"

// Array syntax
echo $data['email']; // "alice@example.com"

// Dot notation
echo $data->get('name'); // "Alice"

Deep Nesting

$data = new FluentValue([
    'user' => [
        'profile' => [
            'name' => 'David',
            'settings' => [
                'theme' => 'dark'
            ]
        ]
    ]
]);

// All nested arrays are automatically wrapped
echo $data->user->profile->name; // "David"
echo $data['user']['profile']['settings']['theme']; // "dark"

// Mix and match syntax
echo $data->user['profile']->settings['theme']; // "dark"

Objects and Value Objects

Objects are preserved (not converted to arrays). You can access public properties or magic accessors:

$model = new class {
    public string $name = 'Ada Lovelace';

    public function __get(string $key): mixed
    {
        return $key === 'role' ? 'admin' : null;
    }
};

$data = new FluentValue(['user' => $model]);

echo $data->user->name; // "Ada Lovelace"
echo $data->user->role; // "admin"

When converting to arrays, objects with a toArray() method are converted using that method.

Lazy Evaluation with Closures

Closures are resolved only when accessed and receive their parent context:

$data = new FluentValue([
    'firstName' => 'John',
    'lastName' => 'Doe',
    'fullName' => function ($parent) {
        return $parent->firstName . ' ' . $parent->lastName;
    }
]);

echo $data->fullName; // "John Doe"

Nested Closures with Parent Access

$data = new FluentValue([
    'company' => 'ACME Corp',
    'department' => [
        'name' => 'Engineering',
        'team' => [
            'name' => 'Backend',
            'description' => function ($parent) {
                return $parent->name . ' team in ' . 
                       $parent->parent()->name . ' at ' . 
                       $parent->parent()->parent()->company;
            }
        ]
    ]
]);

echo $data->department->team->description;
// "Backend team in Engineering at ACME Corp"

Closures Returning Arrays

Closures that return arrays are automatically wrapped. Objects are preserved:

$data = new FluentValue([
    'config' => fn ($parent) => [
        'enabled' => true,
        'settings' => [
            'theme' => 'dark'
        ]
    ]
]);

// Closure result is wrapped in FluentValue
echo $data->config->settings->theme; // "dark"

Pending Overrides (Advanced Feature)

You can set values on unresolved closures, and these overrides will be merged when the closure is resolved:

$data = new FluentValue([
    'config' => fn ($parent) => [
        'theme' => 'dark',
        'timeout' => 30
    ]
]);

// Set values BEFORE the closure is resolved
$data->config->theme = 'light';
$data->config->retries = 3;

// When accessed, overrides are merged with resolved values
echo $data->config->theme;   // "light" (overridden)
echo $data->config->timeout; // 30 (from closure)
echo $data->config->retries; // 3 (new value)

Deep Nesting with Pending Overrides:

$app = new FluentValue([
    'settings' => fn($parent) => [
        'ui' => [
            'theme' => 'dark',
            'layout' => [
                'sidebar' => 'left'
            ]
        ]
    ]
]);

// Set deep nested values before resolution
$app->settings->ui->layout->sidebar = 'right';
$app->settings->ui->layout->footer = 'sticky';
$app->settings->ui->fontSize = 14;

// All overrides are applied when accessed
echo $app->settings->ui->layout->sidebar; // "right"
echo $app->settings->ui->layout->footer;  // "sticky"
echo $app->settings->ui->fontSize;        // 14

Persistence:

Pending overrides persist across multiple accesses:

$data->config->value = 5;

echo $data->config->value; // 5
echo $data->config->value; // Still 5

Unsetting:

Calling unset() on a key clears both the value and any pending overrides:

$data->config->value = 'modified';
unset($data['config']);
// Pending overrides for 'config' are cleared

Advanced Features

Dot Notation

$data = new FluentValue([]);

// Set using dot notation
$data->set('user.profile.name', 'Alice');

// Get using dot notation
echo $data->get('user.profile.name'); // "Alice"

// Check existence
if ($data->has('user.profile.name')) {
    // ...
}

// Get with default
echo $data->get('user.email', 'unknown@example.com');

// Default can be a closure
$email = $data->get('user.email', fn ($parent) => $parent->name . '@example.com');

Modifying Data

$data = new FluentValue(['name' => 'Bob']);

// Object syntax
$data->age = 30;

// Array syntax
$data['email'] = 'bob@example.com';

// Dot notation
$data->set('profile.avatar', 'avatar.jpg');

Collection Methods

$data = new FluentValue([
    'name' => 'Charlie',
    'age' => 25,
    'email' => 'charlie@example.com',
    'verified' => true
]);

// Get only specific keys
$subset = $data->only(['name', 'email']);

// Get all except specific keys
$subset = $data->except(['verified']);

// Map over values
$numbers = FluentValue::make([1, 2, 3, 4, 5]);
$doubled = $numbers->map(fn ($n) => $n * 2);

// Filter values
$filtered = $numbers->filter(fn ($n) => $n > 2);

Checking Empty State

$data = new FluentValue([]);

if ($data->isEmpty()) {
    // ...
}

if ($data->isNotEmpty()) {
    // ...
}

Converting to Array

$data = new FluentValue([
    'name' => 'Diana',
    'greeting' => fn ($parent) => 'Hello, ' . $parent->name
]);

// toArray() resolves all closures
$array = $data->toArray();
// ['name' => 'Diana', 'greeting' => 'Hello, Diana']

// raw() returns data without resolving closures
$raw = $data->raw();
// ['name' => 'Diana', 'greeting' => Closure]

// Objects with toArray() are converted during toArray()
$money = new class {
    public function toArray(): array
    {
        return ['amount' => 1500, 'currency' => 'USD'];
    }
};

$data = new FluentValue(['price' => $money]);
// ['price' => ['amount' => 1500, 'currency' => 'USD']]

JSON Serialization

$data = new FluentValue([
    'name' => 'Eve',
    'age' => 28
]);

// Using json_encode
$json = json_encode($data);

// Using toJson method
$json = $data->toJson(JSON_PRETTY_PRINT);

Iteration

$data = new FluentValue([
    'apple' => 1,
    'banana' => 2,
    'cherry' => 3
]);

// Foreach
foreach ($data as $key => $value) {
    echo "$key: $value\n";
}

// Count
echo count($data); // 3

Real-World Examples

API Response Handling

$response = new FluentValue([
    'status' => 'success',
    'data' => [
        'user' => [
            'id' => 123,
            'name' => 'Alice',
            'email' => 'alice@example.com'
        ]
    ],
    'meta' => [
        'timestamp' => fn () => time()
    ]
]);

if ($response->status === 'success') {
    $user = $response->data->user;
    echo $user->name; // "Alice"
}

Configuration Management

$config = new FluentValue([
    'app' => [
        'name' => 'My App',
        'env' => 'production',
        'debug' => false,
        'url' => fn ($parent) => $parent->env === 'production' 
            ? 'https://myapp.com' 
            : 'http://localhost:8000'
    ],
    'database' => [
        'host' => 'localhost',
        'name' => 'myapp',
        'connection_string' => fn ($parent) => 
            "mysql:host={$parent->host};dbname={$parent->name}"
    ]
]);

echo $config->app->url; // "https://myapp.com"
echo $config->database->connection_string; // "mysql:host=localhost;dbname=myapp"

Form Data Processing

$form = new FluentValue([
    'firstName' => 'John',
    'lastName' => 'Doe',
    'email' => 'john@example.com',
    'fullName' => fn ($parent) => trim($parent->firstName . ' ' . $parent->lastName),
    'displayName' => fn ($parent) => $parent->fullName . ' (' . $parent->email . ')'
]);

echo $form->displayName; // "John Doe (john@example.com)"

Dynamic Configuration Building

Use pending overrides to build up configuration incrementally:

$config = new FluentValue([
    'database' => fn ($parent) => [
        'host' => 'localhost',
        'port' => 3306,
        'name' => 'myapp'
    ]
]);

// Override defaults and add new values
$config->database->host = '127.0.0.1';
$config->database->username = 'root';
$config->database->password = env('DB_PASSWORD');
$config->database->options = [
    'charset' => 'utf8mb4'
];

// All values are merged when accessed
$dbConfig = $config->database->toArray();
// [
//   'host' => '127.0.0.1',        // overridden
//   'port' => 3306,                // from closure
//   'name' => 'myapp',             // from closure
//   'username' => 'root',          // added
//   'password' => '...',           // added
//   'options' => ['charset' => ...]// added
// ]

Requirements

  • PHP 8.2 or higher

License

This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.