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
87 changes: 82 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ real-world PHP projects.
- **Unified Facade (`ArrayKit`)**
- **Traits for DTO & Hooking**
- **Pipeline for Collection Ops**
- **Global Helpers (`functions.php`)**
- **LazyCollection for Generator-Based Flows**
- **ArrayShape Validation Helper**
- **Laravel Compatibility Layer (`LaravelCompat\\Arr`, `LaravelCompat\\Collection`)**
- **Namespaced Helpers + Optional Globals**

## Modules

Expand All @@ -34,6 +37,7 @@ real-world PHP projects.
| **ArraySingle** | Helpers for single-dimensional arrays (set ops, mapWithKeys, countBy, min/max, paginate, duplicates, averages). |
| **ArrayMulti** | Helpers for multi-dimensional arrays (flatten, collapse, depth, keyBy/indexBy, firstWhere, recursive sort/filter). |
| **DotNotation** | Get/set/remove values using dot keys; wildcard support; escaped literal-dot paths; flatten & expand. |
| **ArrayShape** | Lightweight array-shape assertions for row validation (`require`). |
| **BaseArrayHelper** | Internal shared base for consistent API across helpers. |
| **ArraySharedOps** | Internal shared operations used by `ArraySingle` and `ArrayMulti` (`each/every/partition/skip*`). |

Expand All @@ -53,6 +57,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`). |
| **BaseCollectionTrait** | Shared collection behavior. |


Expand All @@ -64,11 +69,12 @@ real-world PHP projects.
| **DTOTrait** | Utility trait for DTO-like behavior: populate, extract, cast arrays/objects easily. |


### Global Helpers
### Helper Functions

| File | Description |
|-------------------|------------------------------------------------------------|
| **functions.php** | Global shortcut functions for frequent array/config tasks. |
| Helper Surface | Description |
|----------------|-------------|
| **`Infocyph\ArrayKit\*`** | Namespaced helper functions (`compare`, `array_get`, `array_set`, `collect`, `chain`) autoloaded by default. |
| **`functions.php`** | Optional global helper variants (manual include when needed). |

### ➤ Facade

Expand All @@ -87,6 +93,18 @@ real-world PHP projects.
composer require infocyph/arraykit
```

```php
<?php
// Namespaced helpers are autoloaded by default.
use function Infocyph\ArrayKit\array_get;
use function Infocyph\ArrayKit\array_set;
use function Infocyph\ArrayKit\collect;
use function Infocyph\ArrayKit\chain;

// Optional: enable global helpers in projects that explicitly want them.
require_once __DIR__ . '/vendor/infocyph/arraykit/src/functions.php';
```

## Quick Examples

### One Facade Entry Point
Expand Down Expand Up @@ -115,6 +133,10 @@ $isList = ArraySingle::isList($list); // true
// Duplicates
$dupes = ArraySingle::duplicates($list); // [2]

// Contains checks
$hasAll = ArraySingle::containsAll($list, [1, 2]); // true
$hasAny = ArraySingle::containsAny($list, [99, 2]); // true

// Pagination
$page = ArraySingle::paginate($list, page:1, perPage:2); // [1, 2]
```
Expand All @@ -128,6 +150,19 @@ $data = [ [1, 2], [3, [4, 5]] ];

// Flatten to one level
$flat = ArrayMulti::flatten($data); // [1, 2, 3, 4, 5]
$flatZero = ArrayMulti::flatten($data, 0); // [[1, 2], [3, [4, 5]]]
$flatOne = ArrayMulti::flatten($data, 1); // [1, 2, 3, [4, 5]]

// Multi-column and query helpers
$sortedMany = ArrayMulti::sortByMany($rows, [
['status', 'asc'],
['created_at', 'desc'],
]);
$active = ArrayMulti::whereStartsWith($rows, 'status', 'act', false);
$match = ArrayMulti::whereLike($rows, 'email', '%@example.com');
$first = ArrayMulti::firstWhereIn($rows, 'role', ['admin', 'editor']);
$uniqueUsers = ArrayMulti::uniqueBy($rows, 'email');
$dupeUsers = ArrayMulti::duplicatesBy($rows, fn ($row) => strtolower((string) ($row['email'] ?? '')));

// Collapse one level
$collapsed = ArrayMulti::collapse($data); // [1, 2, 3, [4, 5]]
Expand Down Expand Up @@ -182,6 +217,13 @@ $config->onGet('secure.key', fn($v) => decrypt($v));
// Use it
$config->setWithHooks('auth.password', 'secret123');
$hashed = $config->getWithHooks('auth.password');

// Typed getters + state helpers
$port = $config->getInt('db.port', 3306);
$config->snapshot('before-runtime');
$config->merge(['app' => ['env' => 'production']]);
$changed = $config->changed('before-runtime');
$config->restore('before-runtime');
```

### Hooked Collection
Expand Down Expand Up @@ -218,8 +260,43 @@ class UserDTO {
$user = new UserDTO();
$user->fromArray(['name' => 'Alice', 'email' => 'alice@example.com']);
$array = $user->toArray();

// Advanced hydration / export
$user->hydrate(['name' => 'Alice'], mapping: ['name' => 'full_name']);
$deep = $user->toArrayDeep();
```

### Lazy + Shape + Compat

```php
use Infocyph\ArrayKit\Array\ArrayShape;
use Infocyph\ArrayKit\ArrayKit;
use Infocyph\ArrayKit\LaravelCompat\Arr;

$lazy = ArrayKit::lazyCollection(range(1, 10))
->filterLazy(fn ($v) => $v % 2 === 0)
->take(3)
->all(); // [2, 4, 6]

$row = ArrayShape::require(
['id' => 1, 'email' => 'a@example.com', 'roles' => ['admin']],
['id' => 'int', 'email' => 'string', 'roles' => 'list<string>'],
);

$data = ['user' => ['name' => 'Alice']];
Arr::set($data, 'user.role', 'admin');
```

## Behavior Notes

- `ArrayMulti::flatten($array, 0)` keeps top-level values unchanged; `1` flattens one level; `INF` fully flattens.
- `ArraySingle::avg()`, `sum()`, `isPositive()`, and `isNegative()` only consider numeric values (non-numeric values are ignored).
- `ArraySingle::paginate()` requires `page >= 1` and `perPage >= 1` (throws `InvalidArgumentException` otherwise).
- Callback-based row helpers (`ArrayMulti::sortBy()`, `sum()`, `maxBy()`, `minBy()`) support `($row, $key)`.
- `DotNotation` treats existing `null` keys/properties as present (does not fall back to defaults).
- `DotNotation::hasWildcard()`, `paths()`, `matches()`, `rename()`, and `move()` are available for wildcard/path operations.
- For untrusted/deep payloads, use bounded traversal variants: `DotNotation::getSafe()`, `ArrayMulti::depthGuarded()`, `flattenGuarded()`, and `sortRecursiveGuarded()`.

## Security

Protected by [PHPForge](https://github.com/infocyph/PHPForge) — an automated quality and security gate for PHP projects.
Expand Down
56 changes: 56 additions & 0 deletions benchmarks/CoreBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ final class CoreBench

private array $queryRows = [];

private array $queryRowsLarge = [];

private array $single = [];

private array $singleAssoc = [];
Expand Down Expand Up @@ -65,6 +67,20 @@ public function setUp(): void
['id' => 5, 'role' => 'viewer'],
];

$this->queryRowsLarge = [];
for ($i = 0; $i < 10000; $i++) {
$this->queryRowsLarge[] = [
'id' => $i + 1,
'group' => 'group-' . ($i % 100),
'role' => match ($i % 4) {
0 => 'admin',
1 => 'editor',
2 => 'viewer',
default => null,
},
];
}

$this->dot = [
'app' => ['name' => 'ArrayKit', 'env' => 'local'],
'db' => [
Expand Down Expand Up @@ -92,18 +108,46 @@ public function benchArrayMultiFlatten(): void
ArrayMulti::flatten($this->nested);
}

#[Subject]
public function benchArrayMultiFlattenDeep(): void
{
ArrayMulti::flatten([[[[1, 2], 3], [4, [5, [6]]]], 7]);
}

#[Subject]
public function benchArrayMultiKeyBy(): void
{
ArrayMulti::keyBy($this->nested, 'id');
}

#[Subject]
public function benchArrayMultiKeyBy10k(): void
{
ArrayMulti::keyBy($this->queryRowsLarge, 'id');
}

#[Subject]
public function benchArrayMultiSkipUntil(): void
{
ArrayMulti::skipUntil($this->nested, static fn(array $row): bool => ($row['id'] ?? 0) >= 3);
}

#[Subject]
public function benchArrayMultiUniqueNested(): void
{
ArrayMulti::unique([
['id' => 1, 'meta' => ['x' => 1]],
['id' => 1, 'meta' => ['x' => 1]],
['id' => 2, 'meta' => ['x' => 2]],
], true);
}

#[Subject]
public function benchArrayMultiWhereIn10k(): void
{
ArrayMulti::whereIn($this->queryRowsLarge, 'role', ['admin', 'editor']);
}

#[Subject]
public function benchArrayMultiWhereInNull(): void
{
Expand Down Expand Up @@ -157,4 +201,16 @@ public function benchDotNotationGet(): void
{
DotNotation::get($this->dot, 'db.options.timeout');
}

#[Subject]
public function benchDotNotationWildcardGet(): void
{
DotNotation::get([
'users' => [
['name' => 'Alice'],
['name' => 'Bob'],
['name' => 'Cara'],
],
], 'users.*.name');
}
}
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
"Infocyph\\ArrayKit\\": "src/"
},
"files": [
"src/namespaced-functions.php",
"src/functions.php"
"src/namespaced-functions.php"
]
},
"autoload-dev": {
Expand Down
60 changes: 53 additions & 7 deletions docs/array-helpers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,17 @@ ArraySingle: Search, Partition, Aggregation
<?php
use Infocyph\ArrayKit\Array\ArraySingle;

$arr = [1, 2, 2, 3, 4, 5];
$arr = [1, 2, 2, 3, 4, 5, 'x'];

$foundKey = ArraySingle::search($arr, fn ($v) => $v === 3); // 3
$hasTwo = ArraySingle::contains($arr, 2); // true
$hasAll = ArraySingle::containsAll($arr, [1, 2, 3]); // true
$hasAny = ArraySingle::containsAny($arr, [99, 3]); // true
[$even, $odd] = ArraySingle::partition($arr, fn ($v) => $v % 2 === 0);
$dupes = ArraySingle::duplicates($arr); // [2]
$unique = ArraySingle::unique($arr); // [1,2,3,4,5]
$sum = ArraySingle::sum($arr); // 17
$avg = ArraySingle::avg($arr); // 17/6
$sum = ArraySingle::sum($arr); // 17 (non-numeric ignored)
$avg = ArraySingle::avg($arr); // 17/6 (non-numeric ignored)
$median = ArraySingle::median($arr); // 2.5
$mode = ArraySingle::mode($arr); // [2]

Expand All @@ -117,13 +120,13 @@ ArraySingle: Numeric and Value Helpers
<?php
use Infocyph\ArrayKit\Array\ArraySingle;

$values = [-2, -1, 0, 1, 2, 3];
$values = [-2, -1, 0, 1, 2, 3, 'x'];

$positive = ArraySingle::positive($values); // [1,2,3]
$negative = ArraySingle::negative($values); // [-2,-1]
$isInt = ArraySingle::isInt([1, 2, 3]); // true
$isPositive = ArraySingle::isPositive([1, 2]); // true
$isNegative = ArraySingle::isNegative([-1, -2]); // true
$isPositive = ArraySingle::isPositive([1, 2, 'x']); // true
$isNegative = ArraySingle::isNegative([-1, -2, 'x']); // true

ArrayMulti: Flattening and Shape
--------------------------------
Expand All @@ -137,6 +140,7 @@ ArrayMulti: Flattening and Shape

$collapse = ArrayMulti::collapse($nested); // [1, 2, 3, [4, 5]]
$flat = ArrayMulti::flatten($nested); // [1,2,3,4,5]
$flatZero = ArrayMulti::flatten($nested, 0); // [[1, 2], [3, [4, 5]]]
$flatOne = ArrayMulti::flatten($nested, 1); // flatten one level
$depth = ArrayMulti::depth($nested); // 3
$flatByKey = ArrayMulti::flattenByKey($nested); // flattened values
Expand All @@ -161,7 +165,13 @@ ArrayMulti: Row Filtering
$nullRole = ArrayMulti::whereNull($rows, 'role');
$notNullRole = ArrayMulti::whereNotNull($rows, 'role');
$between = ArrayMulti::between($rows, 'age', 22, 30);
$betweenAlias = ArrayMulti::whereBetween($rows, 'age', 22, 30);
$starts = ArrayMulti::whereStartsWith($rows, 'name', 'Al');
$ends = ArrayMulti::whereEndsWith($rows, 'name', 'ce');
$contains = ArrayMulti::whereContains($rows, 'name', 'li');
$like = ArrayMulti::whereLike($rows, 'name', 'A%');
$custom = ArrayMulti::whereCallback($rows, fn ($row) => $row['name'] === 'Alice');
$firstRole = ArrayMulti::firstWhereIn($rows, 'role', ['editor', 'admin']);

ArrayMulti: Grouping, Ordering, and Projection
----------------------------------------------
Expand All @@ -181,6 +191,11 @@ ArrayMulti: Grouping, Ordering, and Projection
$indexed = ArrayMulti::keyBy($rows, 'team');
$counts = ArrayMulti::countBy($rows, 'team');
$sorted = ArrayMulti::sortBy($rows, 'score', true); // desc
$sortedByKey = ArrayMulti::sortBy($rows, fn ($row, $key) => $key); // callback receives row + key
$sortedMany = ArrayMulti::sortByMany($rows, [
['team', 'asc'],
['score', 'desc'],
]);
$sortedRecursive = ArrayMulti::sortRecursive($rows);
$scores = ArrayMulti::pluck($rows, 'score'); // [10,30,20]
$transposed = ArrayMulti::transpose($rows);
Expand All @@ -200,6 +215,8 @@ ArrayMulti: Row Set Operations
];

$unique = ArrayMulti::unique($rows);
$uniqueByName = ArrayMulti::uniqueBy($rows, 'name');
$dupesByName = ArrayMulti::duplicatesBy($rows, 'name');
$minScore = ArrayMulti::min($rows, 'id');
$maxScore = ArrayMulti::max($rows, 'id');
$firstHigh = ArrayMulti::firstWhere($rows, 'id', '>=', 2);
Expand All @@ -208,6 +225,30 @@ ArrayMulti: Row Set Operations
$mappedKeys = ArrayMulti::mapWithKeys($rows, fn ($row) => [$row['id'] => $row['name']]);
$reduced = ArrayMulti::reduce($rows, fn ($carry, $row) => $carry + $row['id'], 0);
$sumById = ArrayMulti::sum($rows, 'id');
$sumByCallback = ArrayMulti::sum($rows, fn ($row, $key) => $row['id'] + $key);

ArrayShape Validation
---------------------

Use ``ArrayShape`` for lightweight shape assertions in row pipelines.

.. code-block:: php

<?php
use Infocyph\ArrayKit\Array\ArrayShape;

$row = [
'id' => 10,
'email' => 'a@example.com',
'roles' => ['admin', 'editor'],
];

ArrayShape::require($row, [
'id' => 'int',
'email' => 'string',
'roles' => 'list<string>',
'nickname?' => 'string', // optional key
]);

BaseArrayHelper
---------------
Expand Down Expand Up @@ -282,6 +323,11 @@ Behavior Notes
- ``ArraySingle::isAssoc([])`` is ``false``; empty arrays are treated as non-associative.
- ``ArraySingle::nth($array, $step, $offset)`` starts at ``$offset`` then takes every ``$step`` item.
- ``ArraySingle::unique()`` has loose mode (default) and strict mode.
- ``ArraySingle::avg()``, ``sum()``, ``isPositive()``, and ``isNegative()`` ignore non-numeric values.
- ``ArraySingle::paginate()`` requires ``$page >= 1`` and ``$perPage >= 1``.
- ``ArrayMulti::whereIn()`` / ``whereNotIn()`` treat ``null`` as a real value when the key exists.
- ``ArrayMulti::where()`` uses the global ``compare()`` helper semantics for operators.
- ``ArrayMulti::flatten($array, 0)`` returns unchanged top-level values.
- Use ``depthGuarded()``, ``flattenGuarded()``, and ``sortRecursiveGuarded()`` when processing untrusted/deep inputs.
- ``ArrayMulti`` callback helpers such as ``sortBy()``, ``sum()``, ``maxBy()``, ``minBy()`` support ``($row, $key)``.
- ``ArrayMulti::where()`` uses ``Infocyph\ArrayKit\compare()`` semantics for operators.
- ``BaseArrayHelper::random()`` throws ``InvalidArgumentException`` when requested count exceeds array size.
Loading