Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
![GitHub Code Size](https://img.shields.io/github/languages/code-size/infocyph/ArrayKit)
[![Documentation](https://img.shields.io/badge/Documentation-ArrayKit-blue?logo=readthedocs&logoColor=white)](https://docs.infocyph.com/projects/arraykit/)

**ArrayKit** is a modern **PHP 8.4+** library for elegant, high-performance **array manipulation**, **dot notation
utilities**, **dynamic configuration**, **hookable collections**, and more.
From shallow single arrays to deeply nested data structures — **ArrayKit** provides a fluent, reliable toolkit for
real-world PHP projects.
**ArrayKit** is a modern **PHP 8.4+** data toolkit for **array manipulation**, **dot-path access**, **collections**,
**configuration and dotenv parsing**, **hooks**, and **DTO hydration**. These capabilities work together around the
same application-data boundary: shaping values, accessing nested structures, and carrying that data through runtime
configuration or object models.

## Features at a Glance

Expand All @@ -26,6 +26,7 @@ real-world PHP projects.
- **LazyCollection for Generator-Based Flows**
- **ArrayShape Validation Helper**
- **Compiled Config + Lazy Namespace Cache**
- **Dotenv Parsing + Environment References**
- **Namespaced Helpers + Optional Globals**

## Modules
Expand All @@ -47,6 +48,8 @@ real-world PHP projects.
|---------------------|---------------------------------------------------------------------------------------------------------------------|
| **Config** | Dot-access configuration loader with explicit hook-aware variants (`getWithHooks`, `setWithHooks`, `fillWithHooks`) plus compiled cache export/load and read memoization. |
| **LazyFileConfig** | First-segment lazy loader (`db.host` loads `db.php` on demand) with namespace cache files for structural reads and a flat leaf-index cache for exact scalar lookups. |
| **EnvParser** | Strict dotenv parser with variable expansion and circular-reference detection. |
| **Environment** | Process-environment reader and `EnvReference` factory for deferred configuration values. |
| **BaseConfigTrait** | Shared config logic. |


Expand All @@ -57,7 +60,7 @@ real-world PHP projects.
| **Collection** | OOP array wrapper implementing `ArrayAccess`, `IteratorAggregate`, `Countable`, `JsonSerializable`. |
| **HookedCollection** | Extends `Collection` with **on-get/on-set hooks** for real-time transformation of values. |
| **Pipeline** | Functional-style pipeline for chaining operations on collections. |
| **LazyCollection** | Generator-backed lazy operations (`mapLazy`, `filterLazy`, `chunkLazy`, `take`, `takeUntil`). |
| **LazyCollection** | Repeatable lazy operations (`mapLazy`, `filterLazy`, `chunkLazy`, `take`, `takeUntil`), including one-shot generators and renewable factories. |
| **BaseCollectionTrait** | Shared collection behavior. |


Expand Down Expand Up @@ -253,7 +256,7 @@ echo $collection['role']; // Role: admin
### 🔹 DTO Trait Example

```php
use Infocyph\ArrayKit\traits\DTOTrait;
use Infocyph\ArrayKit\DTO\Concerns\DTOTrait;

class UserDTO {
use DTOTrait;
Expand Down
10 changes: 7 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
{
"name": "infocyph/arraykit",
"description": "A Collection of useful PHP array functions.",
"description": "PHP data and configuration primitives: arrays, collections, dot paths, dotenv, hooks, and DTO hydration.",
"license": "MIT",
"type": "library",
"keywords": [
"collection",
"array",
"config"
"collection",
"config",
"dotenv",
"dot-notation",
"dto",
"pipeline"
],
"authors": [
{
Expand Down
17 changes: 17 additions & 0 deletions docs/collection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,16 @@ LazyCollection
--------------

Use ``LazyCollection`` for generator-backed transformations over large iterables.
Collections built with ``from()`` replay values already read from a one-shot
generator without eagerly materializing the source. That replay cache grows with
the portion consumed, so use ``fromFactory()`` for long-lived or unbounded
sources when each traversal can create a fresh iterable.

.. code-block:: php

<?php
use Infocyph\ArrayKit\ArrayKit;
use Infocyph\ArrayKit\Collection\LazyCollection;

$result = ArrayKit::lazyCollection(range(1, 1000))
->filterLazy(fn ($v) => $v % 2 === 0)
Expand All @@ -280,6 +285,18 @@ Use ``LazyCollection`` for generator-backed transformations over large iterables

// [20, 40, 60, 80, 100]

$events = LazyCollection::from((function (): Generator {
yield 'created';
yield 'updated';
})());

$events->all(); // ['created', 'updated']
$events->all(); // ['created', 'updated']

$fresh = LazyCollection::fromFactory(function (): Generator {
yield from fetchEvents();
});

Terminal calculations:

.. code-block:: php
Expand Down
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ ArrayKit Manual
=================

For `ArrayKit <https://github.com/infocyph/ArrayKit>`_ |version|. Updated on |today|.
ArrayKit provides data and configuration primitives: array helpers, dot paths,
collections, dotenv support, hooks, and DTO hydration.

Licensed under `MIT <https://github.com/infocyph/ArrayKit/blob/main/LICENSE>`_.

Expand Down
1 change: 1 addition & 0 deletions docs/migration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Recent Additions
- ``Config`` adds typed getters (``getString/getInt/getFloat/getBool/getArray/getList/getEnum``), merge/state helpers (``merge/overlay/snapshot/restore/changed``), and ``readonly()`` mode.
- ``Collection`` adds ``immutableProcess()`` / ``pipeImmutable()`` explicit immutable-style pipeline entry.
- ``ArrayKit`` facade adds ``lazyCollection()`` and package now includes ``LazyCollection`` (generator-backed operations).
- ``LazyCollection::from()`` now safely replays consumed values from one-shot iterables; use ``fromFactory()`` for a fresh source on every traversal.
- New optional helper: ``ArrayShape`` validator.

Compatibility Notes
Expand Down
4 changes: 3 additions & 1 deletion docs/quick-usage.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
Quick Usage
===========

This page shows copy-paste examples for common ArrayKit operations.
This page shows copy-paste examples for common ArrayKit operations. The package
keeps its data-focused features together: shape and access arrays, transform
collections, hydrate configuration, and map values into DTOs.

ArrayKit Facade Example
----------------------
Expand Down
1 change: 1 addition & 0 deletions docs/rule-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ LazyCollection
.. code-block:: php
public static function from(iterable $source): self
public static function fromFactory(\Closure $factory): self
public static function make(mixed $data = []): self
public function getIterator(): Traversable
public function cursor(): Generator
Expand Down
6 changes: 3 additions & 3 deletions docs/traits-and-helpers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This page covers reusable building blocks outside the core helper classes:
DTOTrait
--------

Namespace: ``Infocyph\ArrayKit\traits\DTOTrait``
Namespace: ``Infocyph\ArrayKit\DTO\Concerns\DTOTrait``

Main methods:

Expand All @@ -29,7 +29,7 @@ Basic DTO Flow
.. code-block:: php
<?php
use Infocyph\ArrayKit\traits\DTOTrait;
use Infocyph\ArrayKit\DTO\Concerns\DTOTrait;
class UserDTO
{
Expand Down Expand Up @@ -76,7 +76,7 @@ Unknown keys are ignored (no dynamic properties are created):
HookTrait
---------

Namespace: ``Infocyph\ArrayKit\traits\HookTrait``
Namespace: ``Infocyph\ArrayKit\Concerns\HookTrait``

Main methods:

Expand Down
2 changes: 1 addition & 1 deletion src/Collection/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use JsonSerializable;

/**
* Class BucketCollection
* Class Collection
*
* A simple array-based collection that implements common
* interfaces (ArrayAccess, IteratorAggregate, Countable, JsonSerializable).
Expand Down
102 changes: 83 additions & 19 deletions src/Collection/LazyCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,22 @@ private function __construct(private \Closure $factory) {}
*/
public static function from(iterable $source): self
{
return new self(static fn(): iterable => $source);
return new self(self::replayableFactory($source));
}

/**
* Create a lazy collection from a factory that returns a fresh iterable for
* every traversal.
*
* @template TFactoryKey of array-key
* @template TFactoryValue
*
* @param \Closure(): iterable<TFactoryKey, TFactoryValue> $factory
* @return self<TFactoryKey, TFactoryValue>
*/
public static function fromFactory(\Closure $factory): self
{
return new self($factory);
}

/**
Expand All @@ -43,7 +58,7 @@ public static function make(mixed $data = []): self
}

if ($data instanceof Traversable) {
return self::from(self::iterableToArray($data));
return self::fromTraversable($data);
}

if ($data === null) {
Expand Down Expand Up @@ -156,12 +171,12 @@ public function take(int $limit): self
return new self(function () use ($limit): Generator {
$count = 0;
foreach ($this->cursor() as $key => $value) {
if ($count >= $limit) {
break;
}

yield $key => $value;
$count++;

if ($count >= $limit) {
return;
}
}
});
}
Expand All @@ -184,24 +199,73 @@ public function takeUntil(callable $callback): self
}

/**
* Normalize traversable input to array-key arrays for generic safety.
* Normalize traversable input without forcing eager materialization.
*
* @param Traversable<mixed, mixed> $source
* @return array<array-key, mixed>
* @param Traversable<array-key, mixed> $source
* @return self<array-key, mixed>
*/
private static function iterableToArray(Traversable $source): array
private static function fromTraversable(Traversable $source): self
{
$results = [];
foreach ($source as $key => $value) {
if (is_int($key) || is_string($key)) {
$results[$key] = $value;
return new self(self::replayableFactory($source));
}

continue;
}
/**
* Adapt any iterable into a repeatable lazy source without eagerly
* materializing it. Values already consumed from a one-shot iterator are
* memoized, while later values are fetched only when a cursor needs them.
*
* @template TSourceKey of array-key
* @template TSourceValue
*
* @param iterable<TSourceKey, TSourceValue> $source
* @return \Closure(): iterable<TSourceKey, TSourceValue>
*/
private static function replayableFactory(iterable $source): \Closure
{
/** @var list<array{0: TSourceKey, 1: TSourceValue}> $cache */
$cache = [];
$sourceCursor = null;
$sourceAdvancePending = false;
$exhausted = false;

$results[] = $value;
}
return static function () use ($source, &$cache, &$sourceCursor, &$sourceAdvancePending, &$exhausted): Generator {
$position = 0;

while (true) {
if (array_key_exists($position, $cache)) {
[$key, $value] = $cache[$position];
yield $key => $value;
$position++;

continue;
}

if ($exhausted) {
return;
}

$sourceCursor ??= (static function () use ($source): Generator {
yield from $source;
})();

if ($sourceAdvancePending) {
$sourceCursor->next();
$sourceAdvancePending = false;
}

if (!$sourceCursor->valid()) {
$exhausted = true;

return $results;
return;
}

$entry = [$sourceCursor->key(), $sourceCursor->current()];
$cache[] = $entry;
$sourceAdvancePending = true;

yield $entry[0] => $entry[1];
$position++;
}
};
}
}
Loading