diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6537ca4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes index d65baf2..c2f634c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ +.editorconfig export-ignore +.gitignore export-ignore tests export-ignore docs export-ignore .github export-ignore @@ -7,5 +9,7 @@ phpunit.xml export-ignore pint.json export-ignore rector.php export-ignore .gitattributes export-ignore +psalm.xml export-ignore +pest.xml export-ignore -* text eol=lf \ No newline at end of file +* text eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f32cc39..931aef9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @abmmhasan \ No newline at end of file +* @abmmhasan diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6a00f60..e9d271d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,6 @@ version: 2 updates: - - package-ecosystem: "composer" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "composer" + directory: "/" schedule: - interval: "daily" \ No newline at end of file + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1367e88..8454e20 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,20 +4,34 @@ on: schedule: - cron: '0 0 * * 0' push: - branches: [ '*' ] + branches: [ "main", "master" ] pull_request: - branches: [ "main", "master", "develop" ] + branches: [ "main", "master", "develop", "development" ] jobs: + prepare: + name: Prepare CI matrix + runs-on: ubuntu-latest + outputs: + php_versions: ${{ steps.matrix.outputs.php_versions }} + dependency_versions: ${{ steps.matrix.outputs.dependency_versions }} + steps: + - name: Define shared matrix values + id: matrix + run: | + echo 'php_versions=["8.3","8.4","8.5"]' >> "$GITHUB_OUTPUT" + echo 'dependency_versions=["prefer-lowest","prefer-stable"]' >> "$GITHUB_OUTPUT" + run: + needs: prepare runs-on: ${{ matrix.operating-system }} strategy: matrix: operating-system: [ ubuntu-latest ] - php-versions: [ '8.2', '8.3', '8.4' ] - dependency-version: [ prefer-lowest, prefer-stable ] + php-versions: ${{ fromJson(needs.prepare.outputs.php_versions) }} + dependency-version: ${{ fromJson(needs.prepare.outputs.dependency_versions) }} - name: PHP ${{ matrix.php-versions }} - ${{ matrix.operating-system }} - ${{ matrix.dependency-version }} + name: Code Analysis - PHP ${{ matrix.php-versions }} - ${{ matrix.dependency-version }} steps: - name: Checkout uses: actions/checkout@v4 @@ -35,11 +49,56 @@ jobs: - name: Validate Composer run: composer validate --strict + - name: Resolve dependencies (${{ matrix.dependency-version }}) + run: composer update --no-interaction --prefer-dist --no-progress --${{ matrix.dependency-version }} + + - name: Test + run: | + composer test:code + composer test:lint + composer test:refactor + # Skip Psalm on prefer-lowest: older transitive amphp versions can trigger PHP 8.4+ deprecations at startup. + if [ "${{ matrix.dependency-version }}" != "prefer-lowest" ]; then + composer test:security + fi + + analyze: + needs: prepare + name: Security Analysis - PHP ${{ matrix.php-versions }} + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: ${{ fromJson(needs.prepare.outputs.php_versions) }} + permissions: + security-events: write + actions: read + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + tools: composer:v2 + - name: Install dependencies - run: composer install --no-interaction --prefer-dist --optimize-autoloader + run: composer install --no-interaction --prefer-dist --no-progress - - name: Package Audit - run: composer audit + - name: Composer Audit (CVE check) + run: composer audit --no-interaction - - name: Test - run: composer tests \ No newline at end of file + # Run Psalm (Deep Taint Analysis) + - name: Run Psalm Security Scan + run: | + php ./vendor/bin/psalm --config=psalm.xml --security-analysis --threads=1 --report=psalm-results.sarif || true + continue-on-error: true + + - name: Upload Psalm Results + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: psalm-results.sarif + category: "psalm-${{ matrix.php-versions }}" + if: always() && hashFiles('psalm-results.sarif') != '' diff --git a/.gitignore b/.gitignore index 9d8ca8d..5cba62c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,13 @@ test.php composer.lock git-story_media *~ -diffs.txt \ No newline at end of file +*.txt +!docs/requirements.txt +.windsurf +.vscode +.phpunit.cache +var +*.patch +patch.php +.psalm-cache +AI_CONTEXT.md diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..8111fe2 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,18 @@ +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +python: + install: + - requirements: docs/requirements.txt + +sphinx: + configuration: docs/conf.py + +formats: + - pdf + - epub + diff --git a/README.md b/README.md index 2652fe8..8acc24a 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,23 @@ ![Packagist Version](https://img.shields.io/packagist/v/infocyph/arraykit) ![Packagist PHP Version](https://img.shields.io/packagist/dependency-v/infocyph/arraykit/php) ![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.2+** library for elegant, high-performance **array manipulation**, **dot notation +**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. ---- - ## 📦 Features at a Glance -✅ **Single-Dimensional Helpers** -✅ **Multi-Dimensional Helpers** -✅ **Dot Notation Get/Set/Flatten** -✅ **Dynamic Config with Hooks** -✅ **Collection & Hooked Collection** -✅ **Traits for DTO & Hooking** -✅ **Pipeline for Collection Ops** -✅ **Global Helpers (`functions.php`)** - ---- +- **Single-Dimensional Helpers** +- **Multi-Dimensional Helpers** +- **Dot Notation Get/Set/Flatten** +- **Dynamic Config with Hooks** +- **Collection & Hooked Collection** +- **Traits for DTO & Hooking** +- **Pipeline for Collection Ops** +- **Global Helpers (`functions.php`)** ## 📚 Modules @@ -39,17 +36,14 @@ real-world PHP projects. | **DotNotation** | Get/set/remove values using dot keys; flatten & expand nested arrays with dot keys. | | **BaseArrayHelper** | Internal shared base for consistent API across helpers. | ---- - ### ➤ Config System | Class | Description | |---------------------|---------------------------------------------------------------------------------------------------------------------| -| **Config** | Immutable dot-access configuration loader. | +| **Config** | Dot-access configuration loader. | | **DynamicConfig** | Extends `Config` with **on-get/on-set hooks** to transform values dynamically (e.g., encrypt/decrypt, auto-format). | | **BaseConfigTrait** | Shared config logic. | ---- ### ➤ Collections @@ -60,7 +54,6 @@ real-world PHP projects. | **Pipeline** | Functional-style pipeline for chaining operations on collections. | | **BaseCollectionTrait** | Shared collection behavior. | ---- ### ➤ Traits @@ -69,7 +62,6 @@ real-world PHP projects. | **HookTrait** | Generic hook system for on-get/on-set callbacks. Used by `DynamicConfig` & `HookedCollection`. | | **DTOTrait** | Utility trait for DTO-like behavior: populate, extract, cast arrays/objects easily. | ---- ### ➤ Global Helpers @@ -77,13 +69,11 @@ real-world PHP projects. |-------------------|------------------------------------------------------------| | **functions.php** | Global shortcut functions for frequent array/config tasks. | ---- ## ✅ Requirements -* **PHP 8.2** or higher +* **PHP 8.4** or higher ---- ## ⚡ Installation @@ -91,8 +81,6 @@ real-world PHP projects. composer require infocyph/arraykit ``` ---- - ## 🚀 Quick Examples ### 🔹 Single-Dimensional Helpers @@ -112,8 +100,6 @@ $dupes = ArraySingle::duplicates($list); // [2] $page = ArraySingle::paginate($list, page:1, perPage:2); // [1, 2] ``` ---- - ### 🔹 Multi-Dimensional Helpers ```php @@ -134,8 +120,6 @@ $depth = ArrayMulti::depth($data); // 3 $sorted = ArrayMulti::sortRecursive($data); ``` ---- - ### 🔹 Dot Notation ```php @@ -156,8 +140,6 @@ $flat = DotNotation::flatten($user); // [ 'profile.name' => 'Alice', 'profile.email' => 'alice@example.com' ] ``` ---- - ### 🔹 Dynamic Config with Hooks ```php @@ -179,8 +161,6 @@ $config->set('auth.password', 'secret123'); $hashed = $config->get('auth.password'); ``` ---- - ### 🔹 Hooked Collection ```php @@ -200,8 +180,6 @@ $collection['role'] = 'admin'; echo $collection['role']; // Role: admin ``` ---- - ### 🔹 DTO Trait Example ```php @@ -219,14 +197,10 @@ $user->fromArray(['name' => 'Alice', 'email' => 'alice@example.com']); $array = $user->toArray(); ``` ---- - ## 🤝 Support Have a bug or feature idea? Please [open an issue](https://github.com/infocyph/arraykit/issues). ---- - ## 📄 License Licensed under the **MIT License** — use it freely for personal or commercial projects. See [LICENSE](LICENSE) for diff --git a/captainhook.json b/captainhook.json index 4f4b9a5..09484cf 100644 --- a/captainhook.json +++ b/captainhook.json @@ -1,51 +1,55 @@ { - "commit-msg": { - "enabled": false, - "actions": [] - }, - "pre-push": { - "enabled": false, - "actions": [] - }, - "pre-commit": { - "enabled": true, - "actions": [ - { - "action": "composer validate --strict", - "options": [] - }, - { - "action": "composer audit", - "options": [] - }, - { - "action": "composer tests", - "options": [] - } - ] - }, - "prepare-commit-msg": { - "enabled": false, - "actions": [] - }, - "post-commit": { - "enabled": false, - "actions": [] - }, - "post-merge": { - "enabled": false, - "actions": [] - }, - "post-checkout": { - "enabled": false, - "actions": [] - }, - "post-rewrite": { - "enabled": false, - "actions": [] - }, - "post-change": { - "enabled": false, - "actions": [] - } -} \ No newline at end of file + "commit-msg": { + "enabled": false, + "actions": [] + }, + "pre-push": { + "enabled": false, + "actions": [] + }, + "pre-commit": { + "enabled": true, + "actions": [ + { + "action": "composer validate --strict", + "options": [] + }, + { + "action": "composer audit", + "options": [] + }, + { + "action": "composer test:security", + "options": [] + }, + { + "action": "composer tests", + "options": [] + } + ] + }, + "prepare-commit-msg": { + "enabled": false, + "actions": [] + }, + "post-commit": { + "enabled": false, + "actions": [] + }, + "post-merge": { + "enabled": false, + "actions": [] + }, + "post-checkout": { + "enabled": false, + "actions": [] + }, + "post-rewrite": { + "enabled": false, + "actions": [] + }, + "post-change": { + "enabled": false, + "actions": [] + } +} diff --git a/composer.json b/composer.json index 5290497..aee2eb5 100644 --- a/composer.json +++ b/composer.json @@ -1,72 +1,77 @@ { - "name": "infocyph/arraykit", - "description": "A Collection of useful PHP array functions.", - "type": "library", - "license": "MIT", - "keywords": [ - "collection", - "array", - "config" - ], - "authors": [ - { - "name": "abmmhasan", - "email": "abmmhasan@gmail.com" - } - ], - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Infocyph\\ArrayKit\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Infocyph\\ArrayKit\\Tests\\": "tests/" - } - }, - "require": { - "php": ">=8.2" - }, - "minimum-stability": "stable", - "prefer-stable": true, - "config": { - "sort-packages": true, - "optimize-autoloader": true, - "allow-plugins": { - "pestphp/pest-plugin": true - } - }, - "scripts": { - "test:code": "pest --parallel --processes=10", - "test:refactor": "rector process --dry-run", - "test:lint": "pint --test", - "test:hook": [ - "captainhook hook:post-checkout", - "captainhook hook:pre-commit", - "captainhook hook:post-commit", - "captainhook hook:post-merge", - "captainhook hook:post-rewrite", - "captainhook hook:pre-push" + "name": "infocyph/arraykit", + "description": "A Collection of useful PHP array functions.", + "type": "library", + "license": "MIT", + "keywords": [ + "collection", + "array", + "config" ], - "tests": [ - "@test:code", - "@test:lint", - "@test:refactor" + "authors": [ + { + "name": "abmmhasan", + "email": "abmmhasan@gmail.com" + } ], - "git:hook": "captainhook install --only-enabled -nf", - "test": "pest", - "refactor": "rector process", - "lint": "pint", - "post-autoload-dump": "@git:hook" - }, - "require-dev": { - "captainhook/captainhook": "^5.24", - "laravel/pint": "^1.20", - "pestphp/pest": "^3.7", - "rector/rector": "^2.0", - "symfony/var-dumper": "^7.2" - } + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Infocyph\\ArrayKit\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Infocyph\\ArrayKit\\Tests\\": "tests/" + } + }, + "require": { + "php": ">=8.3" + }, + "minimum-stability": "stable", + "prefer-stable": true, + "config": { + "sort-packages": true, + "optimize-autoloader": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } + }, + "scripts": { + "test:code": "@php vendor/bin/pest --parallel --processes=10", + "test:security": "@php vendor/bin/psalm --config=psalm.xml --security-analysis --show-info=false --no-progress --threads=1", + "test:refactor": "@php vendor/bin/rector process --dry-run", + "test:lint": "@php vendor/bin/pint --test", + "test:hook": [ + "captainhook hook:post-checkout", + "captainhook hook:pre-commit", + "captainhook hook:post-commit", + "captainhook hook:post-merge", + "captainhook hook:post-rewrite", + "captainhook hook:pre-push" + ], + "tests": [ + "@test:code", + "@test:lint", + "@test:refactor", + "@test:security" + ], + "git:hook": "captainhook install --only-enabled -nf", + "test": "@php vendor/bin/pest", + "refactor": "@php vendor/bin/rector process", + "lint": "@php vendor/bin/pint", + "security:scan": "@test:security", + "post-autoload-dump": "captainhook install --only-enabled -nf" + }, + "require-dev": { + "captainhook/captainhook": "^5.29.2", + "laravel/pint": "^1.29", + "pestphp/pest": "^4.4.3", + "pestphp/pest-plugin-drift": "^4.1", + "rector/rector": "^2.3.9", + "symfony/var-dumper": "^7.3 || ^8.0.8", + "vimeo/psalm": "^6.16.1" + } } diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..9930de3 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/theme.css b/docs/_static/theme.css new file mode 100644 index 0000000..ae81ea7 --- /dev/null +++ b/docs/_static/theme.css @@ -0,0 +1,3 @@ +.highlight-php .k { + color: #0077aa; /* Example: make PHP keywords a different color */ +} diff --git a/docs/array-helpers.rst b/docs/array-helpers.rst new file mode 100644 index 0000000..11f3e64 --- /dev/null +++ b/docs/array-helpers.rst @@ -0,0 +1,224 @@ +Array Helpers +============= + +ArrayKit ships static helpers grouped by data shape: + +- ``ArraySingle`` for one-dimensional arrays +- ``ArrayMulti`` for nested arrays / row collections +- ``BaseArrayHelper`` for lower-level shared operations + +Choosing the Right Helper +------------------------- + +Use ``ArraySingle`` when your data is a simple list or key-value map: + +.. code-block:: php + + 'prod', 'debug' => false]; + +Use ``ArrayMulti`` for row sets and nested arrays: + +.. code-block:: php + + 1, 'name' => 'Alice'], + ['id' => 2, 'name' => 'Bob'], + ]; + +ArraySingle: Structure and Existence +------------------------------------ + +.. code-block:: php + + 'Alice', 'age' => 30]; + $list = [10, 20, 30]; + + $exists = ArraySingle::exists($arr, 'name'); // true + $isList = ArraySingle::isList($list); // true + $isAssoc = ArraySingle::isAssoc($arr); // true + $isUnique = ArraySingle::isUnique([1, 2, 3]); // true + +ArraySingle: Selection and Transformation +----------------------------------------- + +.. code-block:: php + + 1, 'b' => 2, 'c' => 3, 'd' => 4]; + + $only = ArraySingle::only($arr, ['a', 'd']); // ['a' => 1, 'd' => 4] + $except = ArraySingle::except($arr, ['b']); // ['a' => 1, 'c' => 3, 'd' => 4] + $mapped = ArraySingle::map($arr, fn ($v) => $v * 10); + $filtered = ArraySingle::where($arr, fn ($v) => $v > 2); + $rejected = ArraySingle::reject($arr, fn ($v) => $v > 3); + +ArraySingle: Positional Operations +---------------------------------- + +.. code-block:: php + + 20, 2 => 30, 3 => 40] + $skip = ArraySingle::skip($arr, 2); // [2 => 30, 3 => 40, ...] + $nth = ArraySingle::nth($arr, 2); // [10, 30, 50] + $page = ArraySingle::paginate($arr, 2, 2); // page 2 => [2 => 30, 3 => 40] + $chunks = ArraySingle::chunk($arr, 2); // [[10,20], [30,40], [50,60]] + $until40 = ArraySingle::skipUntil($arr, fn ($v) => $v === 40); + +ArraySingle: Search, Partition, Aggregation +------------------------------------------- + +.. code-block:: php + + $v === 3); // 3 + [$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 + $median = ArraySingle::median($arr); // 2.5 + $mode = ArraySingle::mode($arr); // [2] + +ArraySingle: Numeric and Value Helpers +-------------------------------------- + +.. code-block:: php + + 'Alice', 'age' => 30, 'role' => 'admin'], + ['name' => 'Bob', 'age' => 21, 'role' => null], + ['name' => 'Cara', 'age' => 25, 'role' => 'editor'], + ]; + + $adults = ArrayMulti::where($rows, 'age', '>=', 25); + $inRole = ArrayMulti::whereIn($rows, 'role', ['admin', 'editor']); + $notInRole = ArrayMulti::whereNotIn($rows, 'role', ['guest']); + $nullRole = ArrayMulti::whereNull($rows, 'role'); + $notNullRole = ArrayMulti::whereNotNull($rows, 'role'); + $between = ArrayMulti::between($rows, 'age', 22, 30); + $custom = ArrayMulti::whereCallback($rows, fn ($row) => $row['name'] === 'Alice'); + +ArrayMulti: Grouping, Ordering, and Projection +---------------------------------------------- + +.. code-block:: php + + 'A', 'score' => 10], + ['team' => 'B', 'score' => 30], + ['team' => 'A', 'score' => 20], + ]; + + $grouped = ArrayMulti::groupBy($rows, 'team'); + $sorted = ArrayMulti::sortBy($rows, 'score', true); // desc + $sortedRecursive = ArrayMulti::sortRecursive($rows); + $scores = ArrayMulti::pluck($rows, 'score'); // [10,30,20] + $transposed = ArrayMulti::transpose($rows); + +ArrayMulti: Row Set Operations +------------------------------ + +.. code-block:: php + + 1, 'name' => 'A'], + ['id' => 1, 'name' => 'A'], + ['id' => 2, 'name' => 'B'], + ]; + + $unique = ArrayMulti::unique($rows); + [$passed, $failed] = ArrayMulti::partition($rows, fn ($row) => $row['id'] === 1); + $mapped = ArrayMulti::map($rows, fn ($row) => $row['name']); + $reduced = ArrayMulti::reduce($rows, fn ($carry, $row) => $carry + $row['id'], 0); + $sumById = ArrayMulti::sum($rows, 'id'); + +BaseArrayHelper +--------------- + +``BaseArrayHelper`` includes shared primitives used by higher-level helpers. + +.. code-block:: php + + 1]); // true + + $has = BaseArrayHelper::has(['a' => 1], 'a'); // true + $hasAny = BaseArrayHelper::hasAny(['a' => 1], ['x', 'a']); // true + + $range = BaseArrayHelper::range(1, 5); // [1,2,3,4,5] + $times = BaseArrayHelper::times(3, fn ($i) => "Row $i"); // ['Row 1','Row 2','Row 3'] + $randomOne = BaseArrayHelper::random([10, 20, 30]); + + $any = BaseArrayHelper::any([1, 2, 3], fn ($v) => $v > 2); // true + $all = BaseArrayHelper::all([1, 2, 3], fn ($v) => $v > 0); // true + $key = BaseArrayHelper::findKey(['x' => 3], fn ($v) => $v === 3); // x + +Behavior Notes +-------------- + +- Many methods preserve original keys (especially ``slice``, ``where``, ``skip`` variants). +- ``ArraySingle::unique()`` has loose mode (default) and strict mode. +- ``ArrayMulti::where()`` uses the global ``compare()`` helper semantics for operators. +- ``BaseArrayHelper::random()`` throws ``InvalidArgumentException`` when requested count exceeds array size. diff --git a/docs/collection.rst b/docs/collection.rst new file mode 100644 index 0000000..ea5ac26 --- /dev/null +++ b/docs/collection.rst @@ -0,0 +1,259 @@ +Collections +=========== + +ArrayKit collections provide an object-oriented array wrapper with: + +- dot-notation read/write +- full ``ArrayAccess`` + ``Iterator`` + ``Countable`` behavior +- a chainable pipeline of transformation methods +- optional get/set hooks via ``HookedCollection`` + +Available classes: + +- ``Infocyph\ArrayKit\Collection\Collection`` +- ``Infocyph\ArrayKit\Collection\HookedCollection`` +- ``Infocyph\ArrayKit\Collection\Pipeline`` + +Creating Collections +-------------------- + +.. code-block:: php + + 1, 'b' => 2]); + + // Static factories (accept array-able values) + $c2 = Collection::make(['x' => 10]); + $c3 = Collection::from(['y' => 20]); + + // Global helper (autoloaded from src/functions.php) + $c4 = collect(['z' => 30]); + +Reading and Writing +------------------- + +``Collection`` supports direct array access, dot notation, and helper methods. + +.. code-block:: php + + ['name' => 'Alice'], + 'active' => true, + ]); + + // Dot notation get/set + $name = $c->get('user.name'); // Alice + $c->set('user.email', 'alice@example.com'); + + // ArrayAccess with dot notation + $email = $c['user.email']; // alice@example.com + $c['user.role'] = 'admin'; + + // Multi-key fetch + $subset = $c->get(['user.name', 'user.role']); + + // Existence checks + $hasName = $c->has('user.name'); // true + $hasAny = $c->hasAny(['x', 'user.role']); // true + + // Append with null offset + $c[] = 'tail-value'; + + // Remove key + unset($c['user.role']); + +Collection Utility Methods +-------------------------- + +.. code-block:: php + + 1, 'b' => 2]); + + $all = $c->all(); // full array + $items = $c->items(); // alias of all() + $keys = $c->keys(); // ['a', 'b'] + $array = $c->toArray(); // array output + $json = $c->toJson(); // JSON string + $count = $c->count(); // 2 + $empty = $c->isEmpty(); // false + + $c->merge(['c' => 3]); // now a,b,c + $c->clear(); // now empty + +Iteration and Interfaces +------------------------ + +``Collection`` is directly iterable. + +.. code-block:: php + + 1, 'b' => 2]); + + foreach ($c as $key => $value) { + // $key, $value + } + + // Supports json_encode() through JsonSerializable + $json = json_encode($c); + +HookedCollection +---------------- + +``HookedCollection`` extends ``Collection`` and adds per-key hooks. + +.. code-block:: php + + 'alice', 'user' => ['city' => 'dhaka']]); + + // Run callback(s) when reading key + $c->onGet('name', fn ($v) => strtoupper((string) $v)); + + // Run callback(s) when setting key + $c->onSet('role', fn ($v) => "Role: $v"); + + // Dot-notation hooks are supported + $c->onGet('user.city', fn ($v) => ucfirst((string) $v)); + + echo $c['name']; // ALICE + $c['role'] = 'admin'; + echo $c['role']; // Role: admin + echo $c['user.city']; // Dhaka + +Pipeline Basics +--------------- + +Every transformation method is exposed through ``Pipeline``. +You can start it either with ``process()`` or directly by calling pipeline methods on collection (via ``__call``). + +.. code-block:: php + + filter(fn ($v) => $v > 2) + ->map(fn ($v) => $v * 10) + ->all(); + + // Explicit pipeline: + $sum = $c->process()->sum(); + +Pipeline Methods by Category +---------------------------- + +Selection and filtering: + +- ``only()``, ``except()`` +- ``filter()``, ``reject()`` +- ``where()``, ``whereCallback()`` +- ``whereIn()``, ``whereNotIn()``, ``whereNull()``, ``whereNotNull()`` +- ``between()`` + +Slicing and positional: + +- ``slice()``, ``skip()``, ``skipWhile()``, ``skipUntil()`` +- ``nth()``, ``paginate()``, ``chunk()`` + +Structure and reshape: + +- ``flatten()``, ``flattenByKey()``, ``collapse()`` +- ``groupBy()``, ``pluck()``, ``transpose()`` +- ``wrap()``, ``unWrap()`` + +Ordering and uniqueness: + +- ``sortBy()``, ``sortRecursive()``, ``shuffle()`` +- ``unique()``, ``duplicates()``, ``partition()`` + +Terminal methods (end chain with scalar/array/bool): + +- ``sum()``, ``first()``, ``last()``, ``reduce()`` +- ``any()``, ``median()``, ``mode()``, ``isMultiDimensional()`` + +Flow-control helpers: + +- ``tap()``, ``pipe()``, ``when()``, ``unless()`` + +Detailed Pipeline Examples +-------------------------- + +Filtering and set-style operations: + +.. code-block:: php + + 1, 'name' => 'Alice', 'role' => 'admin', 'age' => 30], + ['id' => 2, 'name' => 'Bob', 'role' => 'editor', 'age' => 21], + ['id' => 3, 'name' => 'Cara', 'role' => null, 'age' => 25], + ]); + + $admins = $users->where('role', 'admin')->all(); + $notNullRole = $users->whereNotNull('role')->all(); + $adultEditors = $users->where('age', '>=', 21)->whereIn('role', ['editor'])->all(); + +Slicing and paging: + +.. code-block:: php + + paginate(1, 2)->all(); // first 2 items + $everySecond = $list->nth(2)->all(); + $skipped = $list->skip(3)->all(); + $until40 = $list->skipUntil(fn ($v) => $v === 40)->all(); + +Grouping and reshaping: + +.. code-block:: php + + 'A', 'score' => 10], + ['team' => 'B', 'score' => 20], + ['team' => 'A', 'score' => 30], + ]); + + $grouped = $rows->groupBy('team')->all(); + $scores = $rows->pluck('score')->all(); // [10, 20, 30] + $sorted = $rows->sortBy('score', desc: true)->all(); + +Terminal calculations: + +.. code-block:: php + + process()->sum(); // 15 + $median = $numbers->process()->median(); // 3 + $mode = $numbers->process()->mode(); // [1,2,3,4,5] (all equal freq) + $hasEven = $numbers->process()->any(fn ($v) => $v % 2 === 0); // true + +Behavior Notes +-------------- + +- Most pipeline methods return the underlying ``Collection`` for chaining. +- Terminal methods return scalar/array/bool and stop the chain. +- Dot-notation works in collection accessors and in ``HookedCollection`` get/set overrides. +- ``merge()`` follows PHP ``array_merge`` semantics (string-key overwrite, numeric append/reindex). diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..b48183b --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,121 @@ +# docs/conf.py — ArrayKit (Sphinx 8.x / Python 3.13, Book Theme) +from __future__ import annotations +import os, datetime +from subprocess import Popen, PIPE + +project = "infocyph/ArrayKit" +author = "Infocyph" +year_now = datetime.date.today().strftime("%Y") +copyright = f"2021-{year_now}" + +def get_version() -> str: + if os.environ.get("READTHEDOCS") == "True": + v = os.environ.get("READTHEDOCS_VERSION") + if v: + return v + try: + pipe = Popen("git rev-parse --abbrev-ref HEAD", stdout=PIPE, shell=True, universal_newlines=True) + v = (pipe.stdout.read() or "").strip() + return v or "latest" + except Exception: + return "latest" + +version = get_version() +release = version +language = "en" +root_doc = "index" # Sphinx 8 + +# --- PHP highlighting -------------------------------------------------------- +from pygments.lexers.web import PhpLexer +from sphinx.highlighting import lexers +highlight_language = "php" +lexers["php"] = PhpLexer(startinline=True) +lexers["php-annotations"] = PhpLexer(startinline=True) + +# --- Extensions -------------------------------------------------------------- +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.todo", + "sphinx.ext.napoleon", + "sphinx.ext.autosectionlabel", + "sphinx.ext.intersphinx", + "sphinx_copybutton", + "sphinx_design", + "sphinxcontrib.phpdomain", + "sphinx.ext.extlinks", +] + +# MyST (Markdown) +myst_enable_extensions = [ + "colon_fence", + "deflist", + "attrs_block", + "attrs_inline", + "tasklist", + "fieldlist", + "linkify", +] +myst_heading_anchors = 3 + +# Autodoc/Napoleon +autodoc_default_options = { + "members": True, + "undoc-members": True, + "show-inheritance": True, +} +napoleon_google_docstring = True +napoleon_numpy_docstring = False + +# Intersphinx (only inventories that exist) +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} + +# PHP manual shortcut: :php:`json_encode` +extlinks = { + "php": ("https://www.php.net/%s", "%s"), +} + +# --- HTML output ------------------------------------------------------------- +html_theme = "sphinx_book_theme" +html_theme_options = { + "repository_url": "https://github.com/infocyph/ArrayKit", + "repository_branch": "main", + "path_to_docs": "docs", + "use_repository_button": True, + "use_issues_button": True, + "use_download_button": True, # PDF/ePub from RTD + "home_page_in_toc": True, + "show_toc_level": 2, # depth in right sidebar +} +templates_path = ["_templates"] +html_static_path = ["_static"] +html_css_files = ["theme.css"] +html_title = f"ArrayKit Manual ({version})" +html_show_sourcelink = True +html_show_sphinx = False +html_last_updated_fmt = "%Y-%m-%d" + +# --- PDF (LaTeX) options (optional) ----------------------------------------- +latex_engine = "xelatex" +latex_elements = { + "papersize": "a4paper", + "pointsize": "11pt", + "preamble": "", + "figure_align": "H", +} + +# --- GitHub context ---------------------------------------------------------- +html_context = { + "display_github": False, # book theme uses the repo buttons above + "github_user": "infocyph", + "github_repo": "ArrayKit", + "github_version": version, + "conf_py_path": "/docs/", +} + +# Replaceable year token for RST +rst_prolog = f""" +.. |current_year| replace:: {year_now} +""" diff --git a/docs/config.rst b/docs/config.rst new file mode 100644 index 0000000..d507401 --- /dev/null +++ b/docs/config.rst @@ -0,0 +1,195 @@ +Configuration +============= + +ArrayKit configuration objects provide dot-notation access to nested settings. + +Classes: + +- ``Infocyph\ArrayKit\Config\Config`` +- ``Infocyph\ArrayKit\Config\DynamicConfig`` + +``DynamicConfig`` extends ``Config`` by adding value hooks. + +Loading Configuration +--------------------- + +You can load config from an array or a PHP file that returns an array. + +.. code-block:: php + + loadArray([ + 'app' => ['name' => 'ArrayKit', 'env' => 'local'], + 'db' => ['host' => 'localhost', 'port' => 3306], + ]); + + // Or from file: + // $ok = $config->loadFile(__DIR__.'/config.php'); + +Important behavior: + +- ``loadArray()`` and ``loadFile()`` only load when config is currently empty. +- If already loaded, they return ``false`` and do not overwrite existing items. + +Reading Values +-------------- + +.. code-block:: php + + loadArray([ + 'app' => ['name' => 'ArrayKit', 'env' => 'local'], + 'queue' => ['driver' => 'sync'], + ]); + + $all = $config->all(); + $name = $config->get('app.name'); // ArrayKit + $fallback = $config->get('app.debug', false); // false + $many = $config->get(['app.name', 'queue.driver']); + + $has = $config->has('app.name'); // true + $hasAny = $config->hasAny(['missing.path', 'queue.driver']); // true + +Writing Values +-------------- + +Single key: + +.. code-block:: php + + set('cache.driver', 'file'); + $config->set('db.port', 5432); + +Bulk set: + +.. code-block:: php + + set([ + 'app.env' => 'production', + 'cache.prefix' => 'arraykit_', + ]); + +Overwrite control: + +.. code-block:: php + + set('app.env', 'local', overwrite: false); + +Fill Missing Values +------------------- + +``fill()`` writes only if target key does not already exist. + +.. code-block:: php + + fill('mail.driver', 'smtp'); + $config->fill([ + 'mail.host' => 'localhost', + 'mail.port' => 1025, + ]); + + // Existing keys are preserved + $config->fill('app.env', 'staging'); + +Removing Values +--------------- + +.. code-block:: php + + forget('cache.prefix'); + $config->forget(['mail.host', 'mail.port']); + +Array-Value Helpers +------------------- + +``prepend()`` and ``append()`` are useful for list-type config nodes. + +.. code-block:: php + + set('middleware', ['auth']); + $config->append('middleware', 'throttle'); + $config->prepend('middleware', 'cors'); + + // ['cors', 'auth', 'throttle'] + $middleware = $config->get('middleware'); + +DynamicConfig Hooks +------------------- + +``DynamicConfig`` allows per-key transformation on read/write. + +.. code-block:: php + + onSet('user.name', fn ($v) => strtoupper((string) $v)); + $config->onGet('user.name', fn ($v) => strtolower((string) $v)); + + $config->set('user.name', 'Alice'); + echo $config->get('user.name'); // alice + +Bulk operations with hooks: + +.. code-block:: php + + onSet('user.email', fn ($v) => trim((string) $v)); + + $config->set([ + 'user.name' => 'JOHN', + 'user.email' => ' john@example.com ', + ]); + + $vals = $config->get(['user.name', 'user.email']); + +Practical Pattern +----------------- + +Use config as a mutable runtime container for app setup: + +.. code-block:: php + + loadFile(__DIR__.'/config.php'); + + // Normalize selected runtime values + $config->onSet('app.timezone', fn ($v) => trim((string) $v)); + $config->onGet('app.timezone', fn ($v) => strtoupper((string) $v)); + + $config->set('app.timezone', ' utc '); + $tz = $config->get('app.timezone'); // UTC + +Method Summary +-------------- + +Config methods: + +- ``loadFile()``, ``loadArray()``, ``all()`` +- ``get()``, ``has()``, ``hasAny()`` +- ``set()``, ``fill()``, ``forget()`` +- ``prepend()``, ``append()`` + +DynamicConfig methods: + +- ``get()`` (hook-aware override) +- ``set()`` (hook-aware override) +- ``fill()`` (hook-aware override) +- ``onGet()``, ``onSet()`` diff --git a/docs/dot-notation.rst b/docs/dot-notation.rst new file mode 100644 index 0000000..ebf95ca --- /dev/null +++ b/docs/dot-notation.rst @@ -0,0 +1,203 @@ +Dot Notation +============ + +``Infocyph\ArrayKit\Array\DotNotation`` is the core nested access utility used across ArrayKit. + +It supports: + +- dot-path reads/writes (``user.profile.name``) +- multi-key reads and bulk set/fill +- wildcard traversal in reads and forget operations +- typed accessors with exceptions +- flatten/expand conversion + +Basic Get/Set +------------- + +.. code-block:: php + + ['profile' => ['name' => 'Alice']]]; + + $name = DotNotation::get($data, 'user.profile.name'); // Alice + $missing = DotNotation::get($data, 'user.profile.email', 'n/a'); // n/a + + DotNotation::set($data, 'user.profile.email', 'alice@example.com'); + + // Replace entire array (key = null) + DotNotation::set($data, null, ['fresh' => true]); + +Reading Multiple Keys +--------------------- + +.. code-block:: php + + ['name' => 'ArrayKit', 'env' => 'local'], + 'db' => ['host' => 'localhost'], + ]; + + $result = DotNotation::get($data, ['app.name', 'db.host'], 'default'); + // [ + // 'app.name' => 'ArrayKit', + // 'db.host' => 'localhost', + // ] + +Fill vs Set +----------- + +``set()`` writes values normally. ``fill()`` only writes when key is missing. + +.. code-block:: php + + ['env' => 'prod']]; + + DotNotation::set($data, 'app.env', 'local'); // overwrite -> local + DotNotation::fill($data, 'app.env', 'staging'); // does not overwrite + DotNotation::fill($data, 'app.debug', true); // writes + +Bulk Set/Fill +------------- + +.. code-block:: php + + 'Alice', + 'user.email' => 'alice@example.com', + ]); + + DotNotation::fill($data, [ + 'user.name' => 'Bob', // ignored + 'user.role' => 'admin', // added + ]); + +Existence Checks +---------------- + +.. code-block:: php + + ['name' => 'Alice']]; + + $has = DotNotation::has($data, 'user.name'); // true + $hasBoth = DotNotation::has($data, ['user.name', 'user.email']); // false + $hasAny = DotNotation::hasAny($data, ['x.y', 'user.name']); // true + +Forgetting Keys +--------------- + +.. code-block:: php + + [ + ['name' => 'A', 'secret' => 'x'], + ['name' => 'B', 'secret' => 'y'], + ], + ]; + + // Remove one nested path + DotNotation::forget($data, 'users.0.secret'); + + // Wildcard remove (all users.*.secret) + DotNotation::forget($data, 'users.*.secret'); + +Wildcards and Special Segments in get() +--------------------------------------- + +.. code-block:: php + + [ + ['name' => 'Alice'], + ['name' => 'Bob'], + ], + ]; + + $names = DotNotation::get($data, 'users.*.name'); // ['Alice', 'Bob'] + $first = DotNotation::get($data, 'users.{first}.name'); // Alice + $last = DotNotation::get($data, 'users.{last}.name'); // Bob + +Flatten and Expand +------------------ + +.. code-block:: php + + ['name' => 'Alice', 'email' => 'alice@example.com'], + ]); + // [ + // 'user.name' => 'Alice', + // 'user.email' => 'alice@example.com', + // ] + + $expanded = DotNotation::expand($flat); + +Typed Accessors +--------------- + +Typed helpers enforce value type and throw ``InvalidArgumentException`` on mismatch. + +.. code-block:: php + + ['name' => 'ArrayKit', 'debug' => true], + 'port' => 8080, + 'ratio' => 0.75, + ]; + + $name = DotNotation::string($cfg, 'app.name'); + $debug = DotNotation::boolean($cfg, 'app.debug'); + $port = DotNotation::integer($cfg, 'port'); + $ratio = DotNotation::float($cfg, 'ratio'); + +ArrayAccess-style Helpers +------------------------- + +``DotNotation`` also exposes static ``offset*`` methods: + +.. code-block:: php + + `_ |version|. Updated on |today|. + +Licensed under `MIT `_. + +Contents +-------- + +.. toctree:: + :maxdepth: 2 + :includehidden: + + installation + array-helpers + dot-notation + collection + config + traits-and-helpers + quick-usage + rule-reference + +The feature pages above are guide-style usage docs. +Use ``API Reference (1:1)`` for the full signature catalog. diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..3ad8310 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,17 @@ +Installation +============ + +You can install ArrayKit via Composer. + +.. code-block:: bash + + composer require infocyph/arraykit + +Requirements +------------ + +ArrayKit has the following requirements: + +* **PHP 8.4+** + +Autoload is PSR-4 based and includes global helper functions from ``src/functions.php``. diff --git a/docs/quick-usage.rst b/docs/quick-usage.rst new file mode 100644 index 0000000..833b81c --- /dev/null +++ b/docs/quick-usage.rst @@ -0,0 +1,79 @@ +Quick Usage +=========== + +This page shows copy-paste examples for common ArrayKit operations. + +ArraySingle Example +------------------- + +.. code-block:: php + + ['name' => 'Alice']]; + $name = DotNotation::get($user, 'profile.name'); // Alice + DotNotation::set($user, 'profile.email', 'alice@example.com'); + DotNotation::forget($user, 'profile.name'); + +Collection + Pipeline Example +----------------------------- + +.. code-block:: php + + filter(fn ($v) => $v % 2 === 0)->all(); // [1 => 2, 3 => 4] + $sum = $collection->process()->sum(); // 10 + +Config + Hooks Example +---------------------- + +.. code-block:: php + + set('auth.password', 'secret'); + $config->onGet('auth.password', fn ($v) => strtoupper((string) $v)); + echo $config->get('auth.password'); // SECRET + +Global Helper Example +--------------------- + +.. code-block:: php + + ['name' => 'Alice']]; + $name = array_get($data, 'user.name'); + array_set($data, 'user.email', 'alice@example.com'); + $c = collect([1, 2, 3]); diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..cddeba8 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,7 @@ +sphinx>=8.0,<9 +myst-parser>=2.0 +sphinx-book-theme>=1.1 +sphinxcontrib-phpdomain>=0.9 +sphinx-copybutton>=0.5.2 +sphinx-design>=0.6 +linkify-it-py>=2.0 diff --git a/docs/rule-reference.rst b/docs/rule-reference.rst new file mode 100644 index 0000000..cd54486 --- /dev/null +++ b/docs/rule-reference.rst @@ -0,0 +1,309 @@ +API Reference (1:1) +=================== + +This page maps the current public API surface in ``src/`` one-to-one. + +Global Helper Functions +----------------------- + +.. code-block:: php + + function compare(mixed $retrieved, mixed $value, ?string $operator = null): bool + function isCallable(mixed $value): bool + function array_get(array $array, int|string|array|null $key = null, mixed $default = null): mixed + function array_set(array &$array, string|array|null $key, mixed $value = null, bool $overwrite = true): bool + function collect(mixed $data = []): Collection + function chain(mixed $data): Pipeline + +Infocyph\ArrayKit\Array\BaseArrayHelper +--------------------------------------- + +.. code-block:: php + + public static function isMultiDimensional(mixed $array): bool + public static function wrap(mixed $value): array + public static function unWrap(mixed $value): mixed + public static function haveAny(array $array, callable $callback): bool + public static function isAll(array $array, callable $callback): bool + public static function findKey(array $array, callable $callback): int|string|null + public static function accessible(mixed $value): bool + public static function has(array $array, int|string|array $keys): bool + public static function hasAny(array $array, int|string|array $keys): bool + public static function range(int $start, int $end, int $step = 1): array + public static function times(int $number, ?callable $callback = null): array + public static function any(array $array, callable $callback): bool + public static function all(array $array, callable $callback): bool + public static function tap(array $array, callable $callback): array + public static function forget(array &$array, int|string|array $keys): void + public static function random(array $array, int $number = null, bool $preserveKeys = false): mixed + public static function doReject(array $array, mixed $callback): array + +Infocyph\ArrayKit\Array\ArraySingle +----------------------------------- + +.. code-block:: php + + public static function exists(array $array, int|string $key): bool + public static function only(array $array, array|string $keys): array + public static function separate(array $array): array + public static function isList(array $array): bool + public static function isAssoc(array $array): bool + public static function prepend(array $array, mixed $value, mixed $key = null): array + public static function isPositive(array $array): bool + public static function isNegative(array $array): bool + public static function shuffle(array $array, ?int $seed = null): array + public static function isInt(array $array): bool + public static function nonEmpty(array $array): array + public static function avg(array $array): float|int + public static function isUnique(array $array): bool + public static function positive(array $array): array + public static function negative(array $array): array + public static function nth(array $array, int $step, int $offset = 0): array + public static function duplicates(array $array): array + public static function paginate(array $array, int $page, int $perPage): array + public static function combine(array $keys, array $values): array + public static function where(array $array, ?callable $callback = null): array + public static function search(array $array, mixed $needle): int|string|null + public static function chunk(array $array, int $size, bool $preserveKeys = false): array + public static function map(array $array, callable $callback): array + public static function each(array $array, callable $callback): array + public static function reduce(array $array, callable $callback, mixed $initial = null): mixed + public static function some(array $array, callable $callback): bool + public static function every(array $array, callable $callback): bool + public static function contains(array $array, mixed $valueOrCallback, bool $strict = false): bool + public static function sum(array $array, ?callable $callback = null): float|int + public static function unique(array $array, bool $strict = false): array + public static function reject(array $array, mixed $callback = true): array + public static function slice(array $array, int $offset, ?int $length = null): array + public static function skip(array $array, int $count): array + public static function skipWhile(array $array, callable $callback): array + public static function skipUntil(array $array, callable $callback): array + public static function partition(array $array, callable $callback): array + public static function mode(array $array): array + public static function median(array $array): float|int + public static function except(array $array, array|string $keys): array + +Infocyph\ArrayKit\Array\ArrayMulti +---------------------------------- + +.. code-block:: php + + public static function only(array $array, array|string $keys): array + public static function collapse(array $array): array + public static function depth(array $array): int + public static function flatten(array $array, float|int $depth = \INF): array + public static function flattenByKey(array $array): array + public static function sortRecursive(array $array, int $options = \SORT_REGULAR, bool $descending = false): array + public static function first(array $array, ?callable $callback = null, mixed $default = null): mixed + public static function last(array $array, ?callable $callback = null, mixed $default = null): mixed + public static function between(array $array, string $key, float|int $from, float|int $to): array + public static function whereCallback(array $array, ?callable $callback = null, mixed $default = null): mixed + public static function where(array $array, string $key, mixed $operator = null, mixed $value = null): array + public static function chunk(array $array, int $size, bool $preserveKeys = false): array + public static function map(array $array, callable $callback): array + public static function each(array $array, callable $callback): array + public static function reduce(array $array, callable $callback, mixed $initial = null): mixed + public static function some(array $array, callable $callback): bool + public static function every(array $array, callable $callback): bool + public static function contains(array $array, mixed $valueOrCallback, bool $strict = false): bool + public static function unique(array $array, bool $strict = false): array + public static function reject(array $array, mixed $callback = true): array + public static function partition(array $array, callable $callback): array + public static function skip(array $array, int $count): array + public static function skipWhile(array $array, callable $callback): array + public static function skipUntil(array $array, callable $callback): array + public static function sum(array $array, string|callable|null $keyOrCallback = null): float|int + public static function whereIn(array $array, string $key, array $values, bool $strict = false): array + public static function whereNotIn(array $array, string $key, array $values, bool $strict = false): array + public static function whereNull(array $array, string $key): array + public static function whereNotNull(array $array, string $key): array + public static function groupBy(array $array, string|callable $groupBy, bool $preserveKeys = false): array + public static function sortBy(array $array, string|callable $by, bool $desc = false, int $options = \SORT_REGULAR): array + public static function sortByDesc(array $array, string|callable $by, int $options = \SORT_REGULAR): array + public static function transpose(array $matrix): array + public static function pluck(array $array, string $column, ?string $indexBy = null): array + +Infocyph\ArrayKit\Array\DotNotation +----------------------------------- + +.. code-block:: php + + public static function flatten(array $array, string $prepend = ''): array + public static function expand(array $array): array + public static function has(array $array, array|string $keys): bool + public static function hasAny(array $array, array|string $keys): bool + public static function get(array $array, array|int|string|null $keys = null, mixed $default = null): mixed + public static function set(array &$array, array|string|null $keys = null, mixed $value = null, bool $overwrite = true): bool + public static function fill(array &$array, array|string $keys, mixed $value = null): void + public static function forget(array &$target, array|string|int|null $keys): void + public static function string(array $array, string $key, mixed $default = null): string + public static function integer(array $array, string $key, mixed $default = null): int + public static function float(array $array, string $key, mixed $default = null): float + public static function boolean(array $array, string $key, mixed $default = null): bool + public static function arrayValue(array $array, string $key, mixed $default = null): array + public static function pluck(array $array, array|string $keys, mixed $default = null): array + public static function all(array $array): array + public static function tap(array $array, callable $callback): array + public static function offsetExists(array $array, string $key): bool + public static function offsetGet(array $array, string $key): mixed + public static function offsetSet(array &$array, string $key, mixed $value): void + public static function offsetUnset(array &$array, string $key): void + +Infocyph\ArrayKit\Collection\Collection +--------------------------------------- + +Collection uses ``BaseCollectionTrait``. Public API: + +.. code-block:: php + + public function __construct(array $data = []) + public static function from(mixed $data): static + public static function make(mixed $data): static + public function __call(string $method, array $arguments): mixed + public function __invoke(): array + public function process(): Pipeline + public function get(string|array $keys): mixed + public function has(string|array $keys): bool + public function hasAny(string|array $keys): bool + public function set(array|string|null $keys = null, mixed $value = null): bool + public function __get(string $key): mixed + public function __set(string $key, mixed $value): void + public function __isset(string $key): bool + public function __unset(string $key): void + public function getArrayableItems(mixed $items): array + public function all(): array + public function items(): array + public function toJson(int $options = 0): string + public function isEmpty(): bool + public function __toString(): string + public function toArray(): array + public function keys(): array + public function __debugInfo(): array + public function clear(): void + public function merge(mixed $items): static + public function offsetExists(mixed $offset): bool + public function offsetGet(mixed $offset): mixed + public function offsetSet(mixed $offset, mixed $value): void + public function offsetUnset(mixed $offset): void + public function getIterator(): Traversable + public function current(): mixed + public function key(): string|int|null + public function next(): void + public function valid(): bool + public function rewind(): void + public function count(): int + public function jsonSerialize(): array + +Infocyph\ArrayKit\Collection\HookedCollection +--------------------------------------------- + +HookedCollection extends ``Collection`` and adds hook behavior (from ``HookTrait``): + +.. code-block:: php + + public function offsetGet(mixed $offset): mixed + public function offsetSet(mixed $offset, mixed $value): void + public function onGet(string $offset, callable $callback): static + public function onSet(string $offset, callable $callback): static + +Infocyph\ArrayKit\Collection\Pipeline +------------------------------------- + +.. code-block:: php + + public function __construct(protected array &$working, private readonly Collection $collection) + public function only(array|string $keys): Collection + public function nth(int $step, int $offset = 0): Collection + public function duplicates(): Collection + public function slice(int $offset, ?int $length = null): Collection + public function paginate(int $page, int $perPage): Collection + public function combine(array $values): Collection + public function map(callable $callback): Collection + public function filter(callable $callback): Collection + public function chunk(int $size, bool $preserveKeys = false): Collection + public function unique(bool $strict = false): Collection + public function reject(mixed $callback = true): Collection + public function skip(int $count): Collection + public function skipWhile(callable $callback): Collection + public function skipUntil(callable $callback): Collection + public function partition(callable $callback): Collection + public function flatten(float|int $depth = \INF): Collection + public function flattenByKey(): Collection + public function sortRecursive(int $options = SORT_REGULAR, bool $descending = false): Collection + public function collapse(): Collection + public function groupBy(string|callable $groupBy, bool $preserveKeys = false): Collection + public function between(string $key, float|int $from, float|int $to): Collection + public function whereCallback(?callable $callback = null, mixed $default = null): Collection + public function where(string $key, mixed $operator = null, mixed $value = null): Collection + public function whereIn(string $key, array $values, bool $strict = false): Collection + public function whereNotIn(string $key, array $values, bool $strict = false): Collection + public function whereNull(string $key): Collection + public function whereNotNull(string $key): Collection + public function sortBy(string|callable $by, bool $desc = false, int $options = SORT_REGULAR): Collection + public function isMultiDimensional(): bool + public function wrap(): Collection + public function unWrap(): Collection + public function shuffle(?int $seed = null): Collection + public function sum(?callable $callback = null): float|int + public function first(?callable $callback = null, mixed $default = null): mixed + public function last(?callable $callback = null, mixed $default = null): mixed + public function reduce(callable $callback, mixed $initial = null): mixed + public function any(callable $callback): bool + public function except(array|string $keys): Collection + public function median(): float|int + public function mode(): array + public function pluck(string $column, ?string $indexBy = null): Collection + public function transpose(): Collection + public function tap(callable $callback): Collection + public function pipe(callable $callback): Collection + public function when(bool $condition, callable $callback, ?callable $default = null): Collection + public function unless(bool $condition, callable $callback, ?callable $default = null): Collection + +Infocyph\ArrayKit\Config\Config +------------------------------- + +Config uses ``BaseConfigTrait``. Public API: + +.. code-block:: php + + public function loadFile(string $path): bool + public function loadArray(array $resource): bool + public function all(): array + public function has(string|array $keys): bool + public function hasAny(string|array $keys): bool + public function get(string|int|array $key = null, mixed $default = null): mixed + public function set(string|array|null $key = null, mixed $value = null, bool $overwrite = true): bool + public function fill(string|array $key, mixed $value = null): bool + public function forget(string|int|array $key): bool + public function prepend(string $key, mixed $value): bool + public function append(string $key, mixed $value): bool + +Infocyph\ArrayKit\Config\DynamicConfig +-------------------------------------- + +DynamicConfig extends Config behavior with hooks and overrides: + +.. code-block:: php + + public function get(int|string|array|null $key = null, mixed $default = null): mixed + public function set(string|array|null $key = null, mixed $value = null, bool $overwrite = true): bool + public function fill(string|array $key, mixed $value = null): bool + public function onGet(string $offset, callable $callback): static + public function onSet(string $offset, callable $callback): static + +Infocyph\ArrayKit\traits\DTOTrait +--------------------------------- + +.. code-block:: php + + public static function create(array $values): static + public function fromArray(array $values): static + public function toArray(): array + +Infocyph\ArrayKit\traits\HookTrait +---------------------------------- + +.. code-block:: php + + public function onGet(string $offset, callable $callback): static + public function onSet(string $offset, callable $callback): static diff --git a/docs/traits-and-helpers.rst b/docs/traits-and-helpers.rst new file mode 100644 index 0000000..f99f14e --- /dev/null +++ b/docs/traits-and-helpers.rst @@ -0,0 +1,203 @@ +Traits and Global Helpers +========================= + +This page covers reusable building blocks outside the core helper classes: + +- ``DTOTrait`` for data-transfer object hydration +- ``HookTrait`` for key-based get/set transforms +- global helper functions from ``src/functions.php`` + +DTOTrait +-------- + +Namespace: ``Infocyph\ArrayKit\traits\DTOTrait`` + +Main methods: + +- ``create(array $values): static`` (static constructor) +- ``fromArray(array $values): static`` (hydrate current instance) +- ``toArray(): array`` (export public properties) + +Basic DTO Flow +~~~~~~~~~~~~~~ + +.. code-block:: php + + 'Alice', + 'email' => 'alice@example.com', + 'age' => 30, + ]); + + $arr = $user->toArray(); + +Incremental Hydration +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + fromArray(['name' => 'Bob']); + $user->fromArray(['age' => 32]); + +Unknown Keys +~~~~~~~~~~~~ + +Unknown keys are ignored (no dynamic properties are created): + +.. code-block:: php + + 'Alice', + 'unknown_field' => 'ignored', + ]); + + // toArray() contains only declared properties + +HookTrait +--------- + +Namespace: ``Infocyph\ArrayKit\traits\HookTrait`` + +Main methods: + +- ``onGet(string $offset, callable $callback): static`` +- ``onSet(string $offset, callable $callback): static`` + +``HookTrait`` is used internally by: + +- ``Infocyph\ArrayKit\Collection\HookedCollection`` +- ``Infocyph\ArrayKit\Config\DynamicConfig`` + +HookedCollection Integration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + 'alice']); + + // get-time transform + $c->onGet('name', fn ($v) => strtoupper((string) $v)); + + // set-time transform + $c->onSet('role', fn ($v) => "Role: $v"); + + echo $c['name']; // ALICE + $c['role'] = 'admin'; + echo $c['role']; // Role: admin + +DynamicConfig Integration +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + onSet('user.email', fn ($v) => trim((string) $v)); + $config->onGet('user.email', fn ($v) => strtolower((string) $v)); + + $config->set('user.email', ' ALICE@EXAMPLE.COM '); + echo $config->get('user.email'); // alice@example.com + +Multiple Hooks on Same Key +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Hooks run in registration order: + +.. code-block:: php + + onSet('username', fn ($v) => trim((string) $v)); + $config->onSet('username', fn ($v) => strtolower((string) $v)); + + $config->set('username', ' ALICE '); // becomes "alice" + +Global Helper Functions +----------------------- + +Global helpers are autoloaded by Composer (``autoload.files``). + +Available helpers: + +- ``compare(mixed $retrieved, mixed $value, ?string $operator = null): bool`` +- ``isCallable(mixed $value): bool`` +- ``array_get(array $array, int|string|array|null $key = null, mixed $default = null): mixed`` +- ``array_set(array &$array, string|array|null $key, mixed $value = null, bool $overwrite = true): bool`` +- ``collect(mixed $data = []): Collection`` +- ``chain(mixed $data): Pipeline`` + +array_get / array_set +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + ['name' => 'Alice']]; + + $name = array_get($data, 'user.name'); // Alice + $missing = array_get($data, 'user.email', 'n/a'); // n/a + + array_set($data, 'user.email', 'alice@example.com'); + array_set($data, [ + 'user.role' => 'admin', + 'user.active' => true, + ]); + +collect / chain +~~~~~~~~~~~~~~~ + +.. code-block:: php + + filter(fn ($v) => $v % 2 === 0)->all(); // [1 => 2, 3 => 4] + + $sum = chain([1, 2, 3])->sum(); // 6 + +compare Helper +~~~~~~~~~~~~~~ + +.. code-block:: php + + '); // true + compare(10, 10, '==='); // true + compare('5', 5, '!=='); // true + compare(10, 10); // true (default ==) + +isCallable Helper +~~~~~~~~~~~~~~~~~ + +``isCallable()`` is stricter than ``is_callable()`` for strings: + +.. code-block:: php + + true); // true + isCallable('strlen'); // false + +When to Use These Helpers +------------------------- + +- Use ``DTOTrait`` for lightweight request/response data objects. +- Use ``HookTrait`` consumers when you need transparent value transforms. +- Use global functions for concise scripting-style access in app code. diff --git a/pest.xml b/pest.xml new file mode 100644 index 0000000..8f88bf3 --- /dev/null +++ b/pest.xml @@ -0,0 +1,14 @@ + + + + + tests/Unit + + + tests/Feature + + + tests/Integration + + + \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..2ca6adc --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +parameters: + level: 3 + paths: + - src + - tests + tmpDir: var/phpstan + parallel: + maximumNumberOfProcesses: 1 diff --git a/phpunit.xml b/phpunit.xml index e2048aa..f504e3d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,6 @@ @@ -8,10 +8,9 @@ ./tests - - ./src + ./src - \ No newline at end of file + diff --git a/pint.json b/pint.json index de30daf..69de9d0 100644 --- a/pint.json +++ b/pint.json @@ -1,9 +1,74 @@ { - "preset": "psr12", - "exclude": [ - "tests" - ], - "notPath": [ - "rector.php" - ] -} \ No newline at end of file + "preset": "psr12", + "exclude": [ + "tests", + "var" + ], + "notPath": [ + "rector.php" + ], + "rules": { + "ordered_imports": { + "imports_order": ["class", "function", "const"], + "sort_algorithm": "alpha" + }, + "no_unused_imports": true, + + "ordered_class_elements": { + "order": [ + "use_trait", + + "case", + + "constant_public", + "constant_protected", + "constant_private", + "constant", + + "property_public_static", + "property_protected_static", + "property_private_static", + "property_static", + + "property_public_readonly", + "property_protected_readonly", + "property_private_readonly", + + "property_public_abstract", + "property_protected_abstract", + + "property_public", + "property_protected", + "property_private", + "property", + + "construct", + "destruct", + "magic", + "phpunit", + + "method_public_abstract_static", + "method_protected_abstract_static", + "method_private_abstract_static", + + "method_public_abstract", + "method_protected_abstract", + "method_private_abstract", + "method_abstract", + + "method_public_static", + "method_public", + + "method_protected_static", + "method_protected", + + "method_private_static", + "method_private", + + "method_static", + "method" + ], + "sort_algorithm": "alpha" + } + } +} diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..7dff260 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rector.php b/rector.php index a8ed1bd..83ea877 100644 --- a/rector.php +++ b/rector.php @@ -3,12 +3,17 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\Set\ValueObject\SetList; return static function (RectorConfig $rectorConfig): void { - $rectorConfig->paths([ - __DIR__.'/src', - ]); + $rectorConfig->paths([__DIR__ . '/src']); + + $setConstant = SetList::class . '::PHP_' . PHP_MAJOR_VERSION . PHP_MINOR_VERSION; + if (!defined($setConstant)) { + $setConstant = SetList::class . '::PHP_84'; + } + $rectorConfig->sets([ - constant("Rector\Set\ValueObject\LevelSetList::UP_TO_PHP_84"), + constant($setConstant), ]); -}; \ No newline at end of file +}; diff --git a/src/Array/ArrayMulti.php b/src/Array/ArrayMulti.php index bc577fa..5d293c7 100644 --- a/src/Array/ArrayMulti.php +++ b/src/Array/ArrayMulti.php @@ -10,25 +10,40 @@ class ArrayMulti { /** - * Select only certain keys from a multidimensional array. - * - * This method is the multidimensional equivalent of ArraySingle::only. + * Filter a 2D array by a single key's comparison (like "where 'age' between 18 and 65"). * - * @param array $array the multidimensional array to select from - * @param array|string $keys the keys to select - * @return array a new array with the selected keys + * @param array $array The 2D array to filter. + * @param string $key The key in each sub-array to compare. + * @param float|int $from The lower bound of the comparison. + * @param float|int $to The upper bound of the comparison. + * @return array The filtered array. */ - public static function only(array $array, array|string $keys): array + public static function between(array $array, string $key, float|int $from, float|int $to): array { - $result = []; - $pick = array_flip((array)$keys); + return array_filter($array, fn ($item) => ArraySingle::exists($item, $key) + && compare($item[$key], $from, '>=') + && compare($item[$key], $to, '<=')); + } - foreach ($array as $item) { - if (is_array($item)) { - $result[] = array_intersect_key($item, $pick); - } + /** + * Break a 2D array into smaller chunks of a specified size. + * + * This function splits the input array into multiple smaller arrays, each + * containing up to the specified number of elements. If the specified size + * is less than or equal to zero, the entire array is returned as a single chunk. + * + * @param array $array The array to be chunked. + * @param int $size The size of each chunk. + * @param bool $preserveKeys Whether to preserve the keys in the chunks. + * + * @return array An array of arrays, each representing a chunk of the original array. + */ + public static function chunk(array $array, int $size, bool $preserveKeys = false): array + { + if ($size <= 0) { + return [$array]; } - return $result; + return array_chunk($array, $size, $preserveKeys); } /** @@ -53,6 +68,31 @@ public static function collapse(array $array): array return $results; } + /** + * Determine if the array contains a given value or if a callback function + * returns true for at least one element. + * + * If the second argument is a callable, it is used as a callback function + * that receives the value and key of each element in the array. If the + * callback returns true, the function returns true. + * + * If the second argument is not a callable, it is used as the value to + * search for in the array. The optional third argument determines whether + * to use strict comparison (===) or loose comparison (==). + * + * @param array $array The array to search. + * @param mixed $valueOrCallback The value to search for, or a callable to apply to each element. + * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). + * @return bool Whether the array contains the given value or whether the callback returned true for at least one element. + */ + public static function contains(array $array, mixed $valueOrCallback, bool $strict = false): bool + { + if (is_callable($valueOrCallback)) { + return static::some($array, $valueOrCallback); + } + return in_array($valueOrCallback, $array, $strict); + } + /** * Determine the depth of a multidimensional array. * @@ -79,6 +119,73 @@ public static function depth(array $array): int return $depth + 1; // zero-based => plus one } + /** + * Execute a callback on each item in the array, returning the original array. + * + * The callback function receives two arguments: the value of the current + * element and its key. The callback should return a value that can be + * evaluated to boolean. If the callback returns false, the iteration is + * broken. Otherwise, the iteration continues. + * + * @param array $array The array to be iterated over. + * @param callable $callback The callback function to apply to each element. + * + * @return array The original array. + */ + public static function each(array $array, callable $callback): array + { + foreach ($array as $key => $row) { + if ($callback($row, $key) === false) { + break; + } + } + return $array; + } + + /** + * Determine if all rows in a 2D array pass the given truth test. + * + * The callback function receives two arguments: the value of the current + * row and its key. It should return true if the condition is met, or false otherwise. + * + * @param array $array The array of rows to evaluate. + * @param callable $callback The callback to apply to each row. + * @return bool Whether all rows passed the truth test. + */ + public static function every(array $array, callable $callback): bool + { + foreach ($array as $key => $row) { + if (!$callback($row, $key)) { + return false; + } + } + return true; + } + + /** + * Return the first item in a 2D array, or single-dim array, depending on usage. + * If a callback is provided, return the first item that matches the callback. + * Otherwise, return the first item in the array. + * + * @param array $array The array to search in. + * @param callable|null $callback The callback to apply to each element. + * @param mixed $default The default value to return if the array is empty. + * @return mixed The first item in the array, or the default value if empty. + */ + public static function first(array $array, ?callable $callback = null, mixed $default = null): mixed + { + if ($callback === null) { + return empty($array) ? $default : reset($array); + } + + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + return $value; + } + } + return $default; + } + /** * Recursively flatten a multidimensional array to a specified depth. * @@ -129,59 +236,43 @@ public static function flattenByKey(array $array): array } /** - * Recursively sort a multidimensional array by keys/values. + * Group a 2D array by a given column or callback. * - * This method takes a multidimensional array and recursively sorts it by - * keys or values. The sorting options and direction are determined by the - * $options and $descending parameters respectively. + * This method takes a 2D array and a key or a callback as parameters. + * It returns a new array containing the grouped data. * - * @param array $array The multidimensional array to sort. - * @param int $options The sorting options. Defaults to SORT_REGULAR. - * @param bool $descending Whether to sort in descending order. Defaults to false. - * @return array The sorted array. - */ - public static function sortRecursive(array $array, int $options = \SORT_REGULAR, bool $descending = false): array - { - foreach ($array as &$value) { - if (is_array($value)) { - $value = static::sortRecursive($value, $options, $descending); - } - } - - if (ArraySingle::isAssoc($array)) { - $descending - ? krsort($array, $options) - : ksort($array, $options); - } else { - $descending - ? rsort($array, $options) - : sort($array, $options); - } - return $array; - } - - /** - * Return the first item in a 2D array, or single-dim array, depending on usage. - * If a callback is provided, return the first item that matches the callback. - * Otherwise, return the first item in the array. + * If the grouping key is a string, it is used as a key in each sub-array to group by. + * If the grouping key is a callable, it is called with each sub-array and its key as arguments, + * and the return value is used as the grouping key. * - * @param array $array The array to search in. - * @param callable|null $callback The callback to apply to each element. - * @param mixed $default The default value to return if the array is empty. - * @return mixed The first item in the array, or the default value if empty. + * If the `$preserveKeys` parameter is true, the original key from the array is preserved + * in the grouped array. Otherwise, the grouped array values are indexed numerically. + * + * @param array $array The array to group. + * @param string|callable $groupBy The key or callback to group by. + * @param bool $preserveKeys Whether to preserve the original key in the grouped array. + * @return array The grouped array. */ - public static function first(array $array, ?callable $callback = null, mixed $default = null): mixed + public static function groupBy(array $array, string|callable $groupBy, bool $preserveKeys = false): array { - if ($callback === null) { - return empty($array) ? $default : reset($array); - } + $results = []; + foreach ($array as $key => $row) { + $gKey = null; + if (is_callable($groupBy)) { + $gKey = $groupBy($row, $key); + } elseif (isset($row[$groupBy])) { + $gKey = $row[$groupBy]; + } else { + $gKey = '_undefined'; + } - foreach ($array as $key => $value) { - if ($callback($value, $key)) { - return $value; + if ($preserveKeys) { + $results[$gKey][$key] = $row; + } else { + $results[$gKey][] = $row; } } - return $default; + return $results; } /** @@ -204,127 +295,90 @@ public static function last(array $array, ?callable $callback = null, mixed $def } /** - * Filter a 2D array by a single key's comparison (like "where 'age' between 18 and 65"). + * Apply a callback to each row in the array, optionally preserving keys. * - * @param array $array The 2D array to filter. - * @param string $key The key in each sub-array to compare. - * @param float|int $from The lower bound of the comparison. - * @param float|int $to The upper bound of the comparison. - * @return array The filtered array. - */ - public static function between(array $array, string $key, float|int $from, float|int $to): array - { - return array_filter($array, fn ($item) => ArraySingle::exists($item, $key) - && compare($item[$key], $from, '>=') - && compare($item[$key], $to, '<=')); - } - - /** - * Filter a 2D array by a custom callback function on each row. + * The callback function receives two arguments: the value of the current + * element and its key. The callback should return the value to be used + * in the resulting array. * - * If no callback is provided, the method will return the entire array. - * If the array is empty and a default value is provided, that value will be returned. + * @param array $array The array to be mapped over. + * @param callable $callback The callback function to apply to each element. * - * @param array $array The 2D array to filter. - * @param callable|null $callback The callback function to apply to each element. - * If null, the method will return the entire array. - * @param mixed $default The default value to return if the array is empty. - * @return mixed The filtered array, or the default value if the array is empty. + * @return array The array with each element transformed by the callback. */ - public static function whereCallback(array $array, ?callable $callback = null, mixed $default = null): mixed + public static function map(array $array, callable $callback): array { - if ($callback === null) { - return empty($array) ? $default : $array; + $results = []; + foreach ($array as $key => $row) { + $results[$key] = $callback($row, $key); } - return array_filter($array, fn ($item, $index) => $callback($item, $index), \ARRAY_FILTER_USE_BOTH); + return $results; } - /** - * Filter a 2D array by a single key's comparison (like "where 'age' > 18"). + * Select only certain keys from a multidimensional array. * - * If the third argument is omitted, the second argument is treated as the value to compare. - * If the third argument is provided, it is used as the operator for the comparison. + * This method is the multidimensional equivalent of ArraySingle::only. * - * @param array $array The 2D array to filter. - * @param string $key The key in each sub-array to compare. - * @param mixed $operator The operator to use for the comparison. If null, the second argument is treated as the value to compare. - * @param mixed $value The value to compare. - * @return array The filtered array. + * @param array $array the multidimensional array to select from + * @param array|string $keys the keys to select + * @return array a new array with the selected keys */ - public static function where(array $array, string $key, mixed $operator = null, mixed $value = null): array + public static function only(array $array, array|string $keys): array { - // If only 2 args, treat second as $value - if ($value === null && $operator !== null) { - $value = $operator; - $operator = null; - } - - return array_filter($array, fn ($item) => ArraySingle::exists($item, $key) && compare($item[$key], $value, $operator)); - } + $result = []; + $pick = array_flip((array)$keys); - /** - * Break a 2D array into smaller chunks of a specified size. - * - * This function splits the input array into multiple smaller arrays, each - * containing up to the specified number of elements. If the specified size - * is less than or equal to zero, the entire array is returned as a single chunk. - * - * @param array $array The array to be chunked. - * @param int $size The size of each chunk. - * @param bool $preserveKeys Whether to preserve the keys in the chunks. - * - * @return array An array of arrays, each representing a chunk of the original array. - */ - public static function chunk(array $array, int $size, bool $preserveKeys = false): array - { - if ($size <= 0) { - return [$array]; + foreach ($array as $item) { + if (is_array($item)) { + $result[] = array_intersect_key($item, $pick); + } } - return array_chunk($array, $size, $preserveKeys); + return $result; } /** - * Apply a callback to each row in the array, optionally preserving keys. - * - * The callback function receives two arguments: the value of the current - * element and its key. The callback should return the value to be used - * in the resulting array. + * Partition the array into two arrays [passed, failed] based on a callback. * - * @param array $array The array to be mapped over. - * @param callable $callback The callback function to apply to each element. + * The method takes an array and a callback as parameters. + * It iterates over the array, applying the callback to each item. + * If the callback returns true, the item is added to the "passed" array. + * If the callback returns false, the item is added to the "failed" array. + * The method returns an array with two elements, the first being the "passed" array, + * and the second being the "failed" array. * - * @return array The array with each element transformed by the callback. + * @param array $array The array to partition. + * @param callable $callback The callback to use for partitioning. + * @return array An array with two elements, the first being the "passed" array, and the second being the "failed" array. */ - public static function map(array $array, callable $callback): array + public static function partition(array $array, callable $callback): array { - $results = []; + $passed = []; + $failed = []; foreach ($array as $key => $row) { - $results[$key] = $callback($row, $key); + if ($callback($row, $key)) { + $passed[$key] = $row; + } else { + $failed[$key] = $row; + } } - return $results; + return [$passed, $failed]; } - /** - * Execute a callback on each item in the array, returning the original array. - * - * The callback function receives two arguments: the value of the current - * element and its key. The callback should return a value that can be - * evaluated to boolean. If the callback returns false, the iteration is - * broken. Otherwise, the iteration continues. - * - * @param array $array The array to be iterated over. - * @param callable $callback The callback function to apply to each element. - * - * @return array The original array. - */ - public static function each(array $array, callable $callback): array + public static function pluck(array $array, string $column, ?string $indexBy = null): array { - foreach ($array as $key => $row) { - if ($callback($row, $key) === false) { - break; + $results = []; + foreach ($array as $row) { + if (!is_array($row) || !array_key_exists($column, $row)) { + continue; + } + $value = $row[$column]; + if ($indexBy !== null && array_key_exists($indexBy, $row)) { + $results[$row[$indexBy]] = $value; + } else { + $results[] = $value; } } - return $array; + return $results; } /** @@ -348,100 +402,6 @@ public static function reduce(array $array, callable $callback, mixed $initial = return $accumulator; } - /** - * Check if the array (of rows) contains at least one row matching a condition - * - * @param array $array The array to search. - * @param callable $callback The callback to apply to each element. - * @return bool Whether at least one element passed the truth test. - */ - public static function some(array $array, callable $callback): bool - { - foreach ($array as $key => $row) { - if ($callback($row, $key)) { - return true; - } - } - return false; - } - - /** - * Determine if all rows in a 2D array pass the given truth test. - * - * The callback function receives two arguments: the value of the current - * row and its key. It should return true if the condition is met, or false otherwise. - * - * @param array $array The array of rows to evaluate. - * @param callable $callback The callback to apply to each row. - * @return bool Whether all rows passed the truth test. - */ - public static function every(array $array, callable $callback): bool - { - foreach ($array as $key => $row) { - if (!$callback($row, $key)) { - return false; - } - } - return true; - } - - /** - * Determine if the array contains a given value or if a callback function - * returns true for at least one element. - * - * If the second argument is a callable, it is used as a callback function - * that receives the value and key of each element in the array. If the - * callback returns true, the function returns true. - * - * If the second argument is not a callable, it is used as the value to - * search for in the array. The optional third argument determines whether - * to use strict comparison (===) or loose comparison (==). - * - * @param array $array The array to search. - * @param mixed $valueOrCallback The value to search for, or a callable to apply to each element. - * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). - * @return bool Whether the array contains the given value or whether the callback returned true for at least one element. - */ - public static function contains(array $array, mixed $valueOrCallback, bool $strict = false): bool - { - if (is_callable($valueOrCallback)) { - return static::some($array, $valueOrCallback); - } - return in_array($valueOrCallback, $array, $strict); - } - - /** - * Return a new array with all duplicate rows removed. - * - * The method takes an array and an optional boolean parameter as arguments. - * If the boolean parameter is not provided, it defaults to false, which means - * loose comparison (==) will be used when checking for duplicate values. - * If the boolean parameter is true, strict comparison (===) will be used. - * - * The method iterates over the array, keeping track of values seen so far - * in an array. If a value is seen for the first time, it is added to the - * results array. If a value is seen again, it is skipped. - * If the value is an array itself, it is serialized before being compared. - * - * @param array $array The array to remove duplicates from. - * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). Defaults to false. - * @return array The array with all duplicate values removed. - */ - public static function unique(array $array, bool $strict = false): array - { - $seen = []; - $results = []; - foreach ($array as $key => $row) { - // If the row is itself an array, we serialize it for comparison: - $compareValue = is_array($row) ? serialize($row) : $row; - if (!in_array($compareValue, $seen, $strict)) { - $seen[] = $compareValue; - $results[$key] = $row; - } - } - return $results; - } - /** * Return an array with all values that do not pass the given callback. * @@ -470,46 +430,37 @@ public static function reject(array $array, mixed $callback = true): array } /** - * Partition the array into two arrays [passed, failed] based on a callback. + * Skip the first $count items of the array and return the remainder. * - * The method takes an array and a callback as parameters. - * It iterates over the array, applying the callback to each item. - * If the callback returns true, the item is added to the "passed" array. - * If the callback returns false, the item is added to the "failed" array. - * The method returns an array with two elements, the first being the "passed" array, - * and the second being the "failed" array. + * The method takes two parameters: the array to skip and the number of items to skip. + * It returns an array with the same type of indices as the input array. * - * @param array $array The array to partition. - * @param callable $callback The callback to use for partitioning. - * @return array An array with two elements, the first being the "passed" array, and the second being the "failed" array. + * @param array $array The array to skip. + * @param int $count The number of items to skip. + * @return array The skipped array. */ - public static function partition(array $array, callable $callback): array + public static function skip(array $array, int $count): array { - $passed = []; - $failed = []; - foreach ($array as $key => $row) { - if ($callback($row, $key)) { - $passed[$key] = $row; - } else { - $failed[$key] = $row; - } - } - return [$passed, $failed]; + return array_slice($array, $count, null, true); } /** - * Skip the first $count items of the array and return the remainder. + * Skip rows until the callback returns true, then keep the remainder. * - * The method takes two parameters: the array to skip and the number of items to skip. - * It returns an array with the same type of indices as the input array. + * The method takes an array and a callback as parameters. + * It iterates over the array, applying the callback to each row. + * As long as the callback returns false, the row is skipped. + * The first row for which the callback returns true is kept, + * and all subsequent rows are also kept. + * The method returns an array with the same type of indices as the input array. * * @param array $array The array to skip. - * @param int $count The number of items to skip. + * @param callable $callback The callback to use for skipping. * @return array The skipped array. */ - public static function skip(array $array, int $count): array + public static function skipUntil(array $array, callable $callback): array { - return array_slice($array, $count, null, true); + return static::skipWhile($array, fn ($row, $key) => !$callback($row, $key)); } /** @@ -542,22 +493,99 @@ public static function skipWhile(array $array, callable $callback): array } /** - * Skip rows until the callback returns true, then keep the remainder. - * - * The method takes an array and a callback as parameters. - * It iterates over the array, applying the callback to each row. - * As long as the callback returns false, the row is skipped. - * The first row for which the callback returns true is kept, - * and all subsequent rows are also kept. - * The method returns an array with the same type of indices as the input array. + * Check if the array (of rows) contains at least one row matching a condition * - * @param array $array The array to skip. - * @param callable $callback The callback to use for skipping. - * @return array The skipped array. + * @param array $array The array to search. + * @param callable $callback The callback to apply to each element. + * @return bool Whether at least one element passed the truth test. */ - public static function skipUntil(array $array, callable $callback): array + public static function some(array $array, callable $callback): bool { - return static::skipWhile($array, fn ($row, $key) => !$callback($row, $key)); + foreach ($array as $key => $row) { + if ($callback($row, $key)) { + return true; + } + } + return false; + } + + /** + * Sort a 2D array by a specified column or using a callback function. + * + * This method sorts an array based on a given column name or a custom callback. + * The sorting can be performed in ascending or descending order, and it allows + * specifying sorting options. + * + * @param array $array The array to sort. + * @param string|callable $by The column key to sort by, or a callable function that returns the value to sort by. + * @param bool $desc Whether to sort in descending order. Defaults to false (ascending order). + * @param int $options The sorting options. Defaults to SORT_REGULAR. + * @return array The sorted array. + */ + public static function sortBy( + array $array, + string|callable $by, + bool $desc = false, + int $options = \SORT_REGULAR, + ): array { + uasort($array, function ($a, $b) use ($by, $desc, $options) { + $valA = is_callable($by) ? $by($a) : ($a[$by] ?? null); + $valB = is_callable($by) ? $by($b) : ($b[$by] ?? null); + + if ($valA === $valB) { + return 0; + } + $comparison = ($valA < $valB) ? -1 : 1; + return $desc ? -$comparison : $comparison; + }); + return $array; + } + + /** + * Sort a 2D array by a specified column or using a callback function, in descending order. + * + * This is a convenience method for calling `sortBy` with the third argument set to true. + * + * @param array $array The array to sort. + * @param string|callable $by The column key to sort by, or a callable function that returns the value to sort by. + * @param int $options The sorting options. Defaults to SORT_REGULAR. + * @return array The sorted array. + */ + public static function sortByDesc(array $array, string|callable $by, int $options = \SORT_REGULAR): array + { + return static::sortBy($array, $by, true, $options); + } + + /** + * Recursively sort a multidimensional array by keys/values. + * + * This method takes a multidimensional array and recursively sorts it by + * keys or values. The sorting options and direction are determined by the + * $options and $descending parameters respectively. + * + * @param array $array The multidimensional array to sort. + * @param int $options The sorting options. Defaults to SORT_REGULAR. + * @param bool $descending Whether to sort in descending order. Defaults to false. + * @return array The sorted array. + */ + public static function sortRecursive(array $array, int $options = \SORT_REGULAR, bool $descending = false): array + { + foreach ($array as &$value) { + if (is_array($value)) { + $value = static::sortRecursive($value, $options, $descending); + } + } + + if (ArraySingle::isAssoc($array)) { + $descending + ? krsort($array, $options) + : ksort($array, $options); + } else { + $descending + ? rsort($array, $options) + : sort($array, $options); + } + return $array; } /** @@ -590,6 +618,107 @@ public static function sum(array $array, string|callable|null $keyOrCallback = n return $total; } + /** + * Transpose a 2D array (matrix). + * + * This method takes a matrix (2D array) and returns a new matrix where the + * rows are converted into columns and vice versa. If the input matrix is empty, + * it returns an empty array. + * + * @param array $matrix The matrix to transpose. + * @return array The transposed matrix. + */ + public static function transpose(array $matrix): array + { + if (empty($matrix)) { + return []; + } + $keys = array_keys(current($matrix)); + $results = array_fill_keys($keys, []); + + foreach ($matrix as $row) { + foreach ($row as $col => $value) { + $results[$col][] = $value; + } + } + return $results; + } + + /** + * Return a new array with all duplicate rows removed. + * + * The method takes an array and an optional boolean parameter as arguments. + * If the boolean parameter is not provided, it defaults to false, which means + * loose comparison (==) will be used when checking for duplicate values. + * If the boolean parameter is true, strict comparison (===) will be used. + * + * The method iterates over the array, keeping track of values seen so far + * in an array. If a value is seen for the first time, it is added to the + * results array. If a value is seen again, it is skipped. + * If the value is an array itself, it is serialized before being compared. + * + * @param array $array The array to remove duplicates from. + * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). Defaults to false. + * @return array The array with all duplicate values removed. + */ + public static function unique(array $array, bool $strict = false): array + { + $seen = []; + $results = []; + foreach ($array as $key => $row) { + // If the row is itself an array, we serialize it for comparison: + $compareValue = is_array($row) ? serialize($row) : $row; + if (!in_array($compareValue, $seen, $strict)) { + $seen[] = $compareValue; + $results[$key] = $row; + } + } + return $results; + } + + /** + * Filter a 2D array by a single key's comparison (like "where 'age' > 18"). + * + * If the third argument is omitted, the second argument is treated as the value to compare. + * If the third argument is provided, it is used as the operator for the comparison. + * + * @param array $array The 2D array to filter. + * @param string $key The key in each sub-array to compare. + * @param mixed $operator The operator to use for the comparison. If null, the second argument is treated as the value to compare. + * @param mixed $value The value to compare. + * @return array The filtered array. + */ + public static function where(array $array, string $key, mixed $operator = null, mixed $value = null): array + { + // If only 2 args, treat second as $value + if ($value === null && $operator !== null) { + $value = $operator; + $operator = null; + } + + return array_filter($array, fn ($item) => ArraySingle::exists($item, $key) && compare($item[$key], $value, $operator)); + } + + /** + * Filter a 2D array by a custom callback function on each row. + * + * If no callback is provided, the method will return the entire array. + * If the array is empty and a default value is provided, that value will be returned. + * + * @param array $array The 2D array to filter. + * @param callable|null $callback The callback function to apply to each element. + * If null, the method will return the entire array. + * @param mixed $default The default value to return if the array is empty. + * @return mixed The filtered array, or the default value if the array is empty. + */ + public static function whereCallback(array $array, ?callable $callback = null, mixed $default = null): mixed + { + if ($callback === null) { + return empty($array) ? $default : $array; + } + return array_filter($array, fn ($item, $index) => $callback($item, $index), \ARRAY_FILTER_USE_BOTH); + } + /** * Filter rows where "column" matches one of the given values. * @@ -626,25 +755,6 @@ public static function whereNotIn(array $array, string $key, array $values, bool ); } - /** - * Filter rows where a column is null. - * - * This method takes a 2D array and a key as parameters. It returns a new array - * containing only the rows where the specified key exists and its value is null. - * - * @param array $array The array to filter. - * @param string $key The key in each sub-array to check for null value. - * @return array The filtered array with rows where the specified key is null. - */ - public static function whereNull(array $array, string $key): array - { - return array_filter( - $array, - fn ($row) - => !empty($row) && array_key_exists($key, $row) && $row[$key] === null, - ); - } - /** * Filter rows where a column is not null. * @@ -661,132 +771,21 @@ public static function whereNotNull(array $array, string $key): array } /** - * Group a 2D array by a given column or callback. - * - * This method takes a 2D array and a key or a callback as parameters. - * It returns a new array containing the grouped data. - * - * If the grouping key is a string, it is used as a key in each sub-array to group by. - * If the grouping key is a callable, it is called with each sub-array and its key as arguments, - * and the return value is used as the grouping key. - * - * If the `$preserveKeys` parameter is true, the original key from the array is preserved - * in the grouped array. Otherwise, the grouped array values are indexed numerically. - * - * @param array $array The array to group. - * @param string|callable $groupBy The key or callback to group by. - * @param bool $preserveKeys Whether to preserve the original key in the grouped array. - * @return array The grouped array. - */ - public static function groupBy(array $array, string|callable $groupBy, bool $preserveKeys = false): array - { - $results = []; - foreach ($array as $key => $row) { - $gKey = null; - if (is_callable($groupBy)) { - $gKey = $groupBy($row, $key); - } elseif (isset($row[$groupBy])) { - $gKey = $row[$groupBy]; - } else { - $gKey = '_undefined'; - } - - if ($preserveKeys) { - $results[$gKey][$key] = $row; - } else { - $results[$gKey][] = $row; - } - } - return $results; - } - - /** - * Sort a 2D array by a specified column or using a callback function. - * - * This method sorts an array based on a given column name or a custom callback. - * The sorting can be performed in ascending or descending order, and it allows - * specifying sorting options. - * - * @param array $array The array to sort. - * @param string|callable $by The column key to sort by, or a callable function that returns the value to sort by. - * @param bool $desc Whether to sort in descending order. Defaults to false (ascending order). - * @param int $options The sorting options. Defaults to SORT_REGULAR. - * @return array The sorted array. - */ - public static function sortBy( - array $array, - string|callable $by, - bool $desc = false, - int $options = \SORT_REGULAR, - ): array { - uasort($array, function ($a, $b) use ($by, $desc, $options) { - $valA = is_callable($by) ? $by($a) : ($a[$by] ?? null); - $valB = is_callable($by) ? $by($b) : ($b[$by] ?? null); - - if ($valA === $valB) { - return 0; - } - $comparison = ($valA < $valB) ? -1 : 1; - return $desc ? -$comparison : $comparison; - }); - return $array; - } - - /** - * Sort a 2D array by a specified column or using a callback function, in descending order. - * - * This is a convenience method for calling `sortBy` with the third argument set to true. - * - * @param array $array The array to sort. - * @param string|callable $by The column key to sort by, or a callable function that returns the value to sort by. - * @param int $options The sorting options. Defaults to SORT_REGULAR. - * @return array The sorted array. - */ - public static function sortByDesc(array $array, string|callable $by, int $options = \SORT_REGULAR): array - { - return static::sortBy($array, $by, true, $options); - } - - /** - * Transpose a 2D array (matrix). + * Filter rows where a column is null. * - * This method takes a matrix (2D array) and returns a new matrix where the - * rows are converted into columns and vice versa. If the input matrix is empty, - * it returns an empty array. + * This method takes a 2D array and a key as parameters. It returns a new array + * containing only the rows where the specified key exists and its value is null. * - * @param array $matrix The matrix to transpose. - * @return array The transposed matrix. + * @param array $array The array to filter. + * @param string $key The key in each sub-array to check for null value. + * @return array The filtered array with rows where the specified key is null. */ - public static function transpose(array $matrix): array - { - if (empty($matrix)) { - return []; - } - $keys = array_keys(current($matrix)); - $results = array_fill_keys($keys, []); - - foreach ($matrix as $row) { - foreach ($row as $col => $value) { - $results[$col][] = $value; - } - } - return $results; - } - - public static function pluck(array $array, string $column, ?string $indexBy = null): array + public static function whereNull(array $array, string $key): array { - $results = []; - foreach ($array as $row) { - if (!is_array($row) || !array_key_exists($column, $row)) { - continue; - } - $value = $row[$column]; - if ($indexBy !== null && array_key_exists($indexBy, $row)) { - $results[$row[$indexBy]] = $value; - } else { - $results[] = $value; - } - } - return $results; + return array_filter( + $array, + fn ($row) + => !empty($row) && array_key_exists($key, $row) && $row[$key] === null, + ); } } diff --git a/src/Array/ArraySingle.php b/src/Array/ArraySingle.php index 0e9890c..ac5f317 100644 --- a/src/Array/ArraySingle.php +++ b/src/Array/ArraySingle.php @@ -7,149 +7,188 @@ class ArraySingle { /** - * Check if a given key exists in a single-dimensional array. - * - * This method determines whether the specified key is present - * in the array, either by checking if it is set or if it exists - * as a key in the array. + * Calculate the average of an array of numbers. * - * @param array $array The array to search in. - * @param int|string $key The key to check for existence. - * @return bool True if the key exists in the array, false otherwise. + * @param array $array The array of numbers to average. + * @return float|int The average of the numbers in the array. If the array is empty, 0 is returned. */ - public static function exists(array $array, int|string $key): bool + public static function avg(array $array): float|int { - return isset($array[$key]) || array_key_exists($key, $array); + if (empty($array)) { + return 0; + } + return array_sum($array) / count($array); } /** - * Select only certain keys from a single-dimensional array. + * Break an array into smaller chunks of a specified size. * - * This method is the single-dimensional equivalent of ArrayMulti::only. + * This function splits the input array into multiple smaller arrays, each + * containing up to the specified number of elements. If the specified size + * is less than or equal to zero, the entire array is returned as a single chunk. * - * @param array $array The array to select from. - * @param array|string $keys The keys to select. - * @return array A new array with the selected keys. + * @param array $array The array to be chunked. + * @param int $size The size of each chunk. + * @param bool $preserveKeys Whether to preserve the keys in the chunks. + * + * @return array An array of arrays, each representing a chunk of the original array. */ - public static function only(array $array, array|string $keys): array + public static function chunk(array $array, int $size, bool $preserveKeys = false): array { - return array_intersect_key($array, array_flip((array)$keys)); + if ($size <= 0) { + return [$array]; + } + return array_chunk($array, $size, $preserveKeys); } /** - * Split an array into separate arrays of keys and values. + * Combine two arrays into one array with corresponding key-value pairs. * - * Useful for destructuring an array into separate key and value arrays. + * The function takes two arrays, one of keys and one of values, and combines them + * into a single array. If the two arrays are not of equal length, the function + * will truncate the longer array to match the length of the shorter array. * - * @param array $array The array to split. - * @return array A new array containing two child arrays: 'keys' and 'values'. - * @example - * $data = ['a' => 1, 'b' => 2, 'c' => 3]; - * $keysAndValues = ArraySingle::separate($data); - * // $keysAndValues === ['keys' => ['a', 'b', 'c'], 'values' => [1, 2, 3]]; + * @param array $keys The array of keys. + * @param array $values The array of values. + * + * @return array The combined array. */ - public static function separate(array $array): array + public static function combine(array $keys, array $values): array { - return [ - 'keys' => array_keys($array), - 'values' => array_values($array), - ]; + $keyCount = count($keys); + $valueCount = count($values); + + if ($keyCount !== $valueCount) { + $size = ($keyCount > $valueCount) ? $valueCount : $keyCount; + $keys = array_slice($keys, 0, $size); + $values = array_slice($values, 0, $size); + } + + return array_combine($keys, $values) ?: []; } /** - * Determine if an array is a strict list (i.e., has no string keys). + * Determine if the array contains a given value or if a callback function + * returns true for at least one element. * - * A strict list is an array where all keys are integers and are in sequence - * from 0 to n-1, where n is the length of the array. + * If the second argument is a callable, it is used as a callback function + * that receives the value and key of each element in the array. If the + * callback returns true, the function returns true. * - * @param array $array The array to test. - * @return bool True if the array is a strict list, false otherwise. + * If the second argument is not a callable, it is used as the value to + * search for in the array. The optional third argument determines whether + * to use strict comparison (===) or loose comparison (==). + * + * @param array $array The array to search. + * @param mixed $valueOrCallback The value to search for, or a callable to apply to each element. + * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). + * @return bool Whether the array contains the given value or whether the callback returned true for at least one element. */ - public static function isList(array $array): bool + public static function contains(array $array, mixed $valueOrCallback, bool $strict = false): bool { - return static::exists($array, 0) - && array_keys($array) === range(0, count($array) - 1); + if (is_callable($valueOrCallback)) { + return static::some($array, $valueOrCallback); + } + return in_array($valueOrCallback, $array, $strict); } /** - * Determine if an array is an associative array (i.e., has string keys). + * Retrieve duplicate values from an array. * - * An associative array is an array where at least one key is a string. + * This method returns an array of values that occur more than once in the input array. * - * @param array $array The array to test. - * @return bool True if the array is an associative array, false otherwise. + * @param array $array The array to search for duplicates. + * @return array An array of duplicate values. */ - public static function isAssoc(array $array): bool + public static function duplicates(array $array): array { - return array_keys($array) !== range(0, count($array) - 1); + $duplicates = []; + foreach (array_count_values($array) as $value => $count) { + if ($count > 1) { + $duplicates[] = $value; + } + } + return $duplicates; } /** - * Prepend a value to the beginning of an array. + * Execute a callback on each item in the array, returning the original array. * - * If the second parameter is null, the value is prepended as the first element - * in the array. If the second parameter is a key, the value is prepended with - * that key. + * The callback function receives two arguments: the value of the current + * element and its key. The callback should return a value that can be + * evaluated to boolean. If the callback returns false, the iteration is + * broken. Otherwise, the iteration continues. * - * @param array $array The array to prepend to. - * @param mixed $value The value to prepend. - * @param mixed $key The key to prepend with. If null, the value is prepended as the first element. - * @return array The modified array. + * @param array $array The array to be iterated over. + * @param callable $callback The callback function to apply to each element. + * + * @return array The original array. */ - public static function prepend(array $array, mixed $value, mixed $key = null): array + public static function each(array $array, callable $callback): array { - if ($key === null) { - array_unshift($array, $value); - } else { - $array = [$key => $value] + $array; + foreach ($array as $key => $value) { + if ($callback($value, $key) === false) { + break; + } } return $array; } /** - * Determine if all values in the array are positive numbers. + * Determine if all elements in the array pass the given truth test. * - * @param array $array The array to check. - * @return bool True if all values are positive, false otherwise. + * @param array $array The array to search. + * @param callable $callback The callback to apply to each element. + * @return bool Whether all elements passed the truth test. */ - public static function isPositive(array $array): bool + public static function every(array $array, callable $callback): bool { - return !empty($array) && min($array) > 0; + foreach ($array as $key => $value) { + if (!$callback($value, $key)) { + return false; + } + } + return true; } /** - * Determine if all values in the array are negative numbers. + * Get all items from the array except for those with the specified keys. * - * @param array $array The array to check. - * @return bool True if all values are negative, false otherwise. + * @param array $array The array to select from. + * @param array|string $keys The keys to exclude. + * @return array A new array with all items except for those with the specified keys. */ - public static function isNegative(array $array): bool + public static function except(array $array, array|string $keys): array { - return !empty($array) && max($array) < 0; + return array_diff_key($array, array_flip((array) $keys)); + } + /** + * Check if a given key exists in a single-dimensional array. + * + * This method determines whether the specified key is present + * in the array, either by checking if it is set or if it exists + * as a key in the array. + * + * @param array $array The array to search in. + * @param int|string $key The key to check for existence. + * @return bool True if the key exists in the array, false otherwise. + */ + public static function exists(array $array, int|string $key): bool + { + return isset($array[$key]) || array_key_exists($key, $array); } /** - * Randomly shuffles the elements in the given array. + * Determine if an array is an associative array (i.e., has string keys). * - * If no seed is given, the internal PHP random number generator is used. - * If a seed is given, the Mersenne Twister random number generator is - * seeded with the given value, used to shuffle the array, and then reset - * to the current internal PHP random number generator seed. + * An associative array is an array where at least one key is a string. * - * @param array $array The array to shuffle. - * @param int|null $seed Optional seed for the Mersenne Twister. - * @return array The shuffled array. + * @param array $array The array to test. + * @return bool True if the array is an associative array, false otherwise. */ - public static function shuffle(array $array, ?int $seed = null): array + public static function isAssoc(array $array): bool { - if ($seed === null) { - \shuffle($array); - } else { - \mt_srand($seed); - \shuffle($array); - \mt_srand(); - } - return $array; + return array_keys($array) !== range(0, count($array) - 1); } /** @@ -169,397 +208,279 @@ public static function isInt(array $array): bool } /** - * Get only the non-empty values from the array. - * - * A value is considered non-empty if it is not an empty string. + * Determine if an array is a strict list (i.e., has no string keys). * - * @param array $array The array to check. - * @return array The non-empty values. - */ - public static function nonEmpty(array $array): array - { - return array_values(static::where($array, 'strlen')); - } - - /** - * Calculate the average of an array of numbers. + * A strict list is an array where all keys are integers and are in sequence + * from 0 to n-1, where n is the length of the array. * - * @param array $array The array of numbers to average. - * @return float|int The average of the numbers in the array. If the array is empty, 0 is returned. + * @param array $array The array to test. + * @return bool True if the array is a strict list, false otherwise. */ - public static function avg(array $array): float|int + public static function isList(array $array): bool { - if (empty($array)) { - return 0; - } - return array_sum($array) / count($array); + return array_is_list($array); } /** - * Determine if all values in the array are unique. + * Determine if all values in the array are negative numbers. * * @param array $array The array to check. - * @return bool True if all values are unique, false otherwise. + * @return bool True if all values are negative, false otherwise. */ - public static function isUnique(array $array): bool + public static function isNegative(array $array): bool { - return count($array) === count(array_flip($array)); + return !empty($array) && max($array) < 0; } /** - * Get only the positive numeric values from the array. + * Determine if all values in the array are positive numbers. * * @param array $array The array to check. - * @return array The positive numeric values. + * @return bool True if all values are positive, false otherwise. */ - public static function positive(array $array): array + public static function isPositive(array $array): bool { - return static::where($array, static fn ($value) => is_numeric($value) && $value > 0); + return !empty($array) && min($array) > 0; } /** - * Get only the negative numeric values from the array. + * Determine if all values in the array are unique. * * @param array $array The array to check. - * @return array The negative numeric values. + * @return bool True if all values are unique, false otherwise. */ - public static function negative(array $array): array + public static function isUnique(array $array): bool { - return static::where($array, static fn ($value) => is_numeric($value) && $value < 0); + return count($array) === count(array_flip($array)); } /** - * Get every n-th element from the array + * Apply a callback to each item in the array, optionally preserving keys. * - * @param array $array The array to slice. - * @param int $step The "step" value (i.e. the interval between selected elements). - * @param int $offset The offset from which to begin selecting elements. + * The callback function receives two arguments: the value of the current + * element and its key. The callback should return the value to be used + * in the resulting array. * - * @return array The sliced array. + * @param array $array The array to be mapped over. + * @param callable $callback The callback function to apply to each element. + * + * @return array The array with each element transformed by the callback. */ - public static function nth(array $array, int $step, int $offset = 0): array + public static function map(array $array, callable $callback): array { $results = []; - $position = 0; - - foreach ($array as $item) { - if ($position % $step === $offset) { - $results[] = $item; - } - $position++; + foreach ($array as $key => $value) { + $results[$key] = $callback($value, $key); } - return $results; } /** - * Retrieve duplicate values from an array. + * Calculate the median of an array of numbers. * - * This method returns an array of values that occur more than once in the input array. + * The median is the middle value in a sorted list of numbers. If the list has an + * odd number of elements, the median is the element at the middle index. If the list + * has an even number of elements, the median is the average of the two middle elements. * - * @param array $array The array to search for duplicates. - * @return array An array of duplicate values. + * @param array $array The array of numbers to find the median of. + * @return float|int The median of the numbers in the array. If the array is empty, 0 is returned. */ - public static function duplicates(array $array): array + public static function median(array $array): float|int { - $duplicates = []; - foreach (array_count_values($array) as $value => $count) { - if ($count > 1) { - $duplicates[] = $value; - } + if ($array === []) { + return 0; } - return $duplicates; - } + $values = $array; + sort($values, SORT_NUMERIC); + $count = count($values); + $mid = intdiv($count, 2); - /** - * "Paginate" the array by slicing it into a smaller segment. - * - * @param array $array The array to paginate. - * @param int $page The page number to retrieve (1-indexed). - * @param int $perPage The number of items per page. - * - * @return array The paginated slice of the array. - */ - public static function paginate(array $array, int $page, int $perPage): array - { - return array_slice( - $array, - max(0, ($page - 1) * $perPage), - $perPage, - true, - ); + return ($count % 2) + ? $values[$mid] + : ($values[$mid - 1] + $values[$mid]) / 2; } /** - * Combine two arrays into one array with corresponding key-value pairs. - * - * The function takes two arrays, one of keys and one of values, and combines them - * into a single array. If the two arrays are not of equal length, the function - * will truncate the longer array to match the length of the shorter array. + * Find the mode(s) of the array. * - * @param array $keys The array of keys. - * @param array $values The array of values. + * The mode is the value that appears most frequently in the array. + * If there are multiple modes, all of them are returned. * - * @return array The combined array. + * @param array $array The array to find the mode(s) of. + * @return array The mode(s) of the array. */ - public static function combine(array $keys, array $values): array + public static function mode(array $array): array { - $keyCount = count($keys); - $valueCount = count($values); - - if ($keyCount !== $valueCount) { - $size = ($keyCount > $valueCount) ? $valueCount : $keyCount; - $keys = array_slice($keys, 0, $size); - $values = array_slice($values, 0, $size); + if ($array === []) { + return []; } - - return array_combine($keys, $values) ?: []; - } - - /** - * Filter the array using a callback function. - * - * If the callback is omitted, the function will return all elements in the - * array that are truthy. - * - * @param array $array The array to search. - * @param callable|null $callback The callback function to use for filtering. - * This function should take two arguments, the value and the key of each - * element in the array. The function should return true for elements that - * should be kept, and false for elements that should be discarded. - * - * @return array The filtered array. - */ - public static function where(array $array, ?callable $callback = null): array - { - $flag = ($callback !== null) ? \ARRAY_FILTER_USE_BOTH : 0; - return array_filter($array, $callback ?? fn ($val) => (bool)$val, $flag); + $freq = array_count_values($array); + $max = max($freq); + return array_keys(array_filter($freq, fn ($c) => $c === $max)); } /** - * Search the array for a given value and return its key if found. - * - * If the value is a callable, it will be called for each element in the array, - * and if the callback returns true, the key will be returned. If the value is - * not a callable, the function will search for the value in the array using - * strict comparison. If the value is found, its key will be returned. If the - * value is not found, null will be returned. - * - * @param array $array The array to search. - * @param mixed $needle The value to search for, or a callable to use for - * searching. + * Get only the negative numeric values from the array. * - * @return int|string|null The key of the value if found, or null if not found. + * @param array $array The array to check. + * @return array The negative numeric values. */ - public static function search(array $array, mixed $needle): int|string|null + public static function negative(array $array): array { - if (is_callable($needle)) { - foreach ($array as $key => $value) { - if ($needle($value, $key) === true) { - return $key; - } - } - return null; - } - $foundKey = array_search($needle, $array, true); - return $foundKey === false ? null : $foundKey; + return static::where($array, static fn ($value) => is_numeric($value) && $value < 0); } /** - * Break an array into smaller chunks of a specified size. - * - * This function splits the input array into multiple smaller arrays, each - * containing up to the specified number of elements. If the specified size - * is less than or equal to zero, the entire array is returned as a single chunk. + * Get only the non-empty values from the array. * - * @param array $array The array to be chunked. - * @param int $size The size of each chunk. - * @param bool $preserveKeys Whether to preserve the keys in the chunks. + * A value is considered non-empty if it is not an empty string. * - * @return array An array of arrays, each representing a chunk of the original array. + * @param array $array The array to check. + * @return array The non-empty values. */ - public static function chunk(array $array, int $size, bool $preserveKeys = false): array + public static function nonEmpty(array $array): array { - if ($size <= 0) { - return [$array]; - } - return array_chunk($array, $size, $preserveKeys); + return array_values(static::where($array, 'strlen')); } /** - * Apply a callback to each item in the array, optionally preserving keys. - * - * The callback function receives two arguments: the value of the current - * element and its key. The callback should return the value to be used - * in the resulting array. + * Get every n-th element from the array * - * @param array $array The array to be mapped over. - * @param callable $callback The callback function to apply to each element. + * @param array $array The array to slice. + * @param int $step The "step" value (i.e. the interval between selected elements). + * @param int $offset The offset from which to begin selecting elements. * - * @return array The array with each element transformed by the callback. + * @return array The sliced array. */ - public static function map(array $array, callable $callback): array + public static function nth(array $array, int $step, int $offset = 0): array { $results = []; - foreach ($array as $key => $value) { - $results[$key] = $callback($value, $key); + $position = 0; + + foreach ($array as $item) { + if ($position % $step === $offset) { + $results[] = $item; + } + $position++; } + return $results; } /** - * Execute a callback on each item in the array, returning the original array. - * - * The callback function receives two arguments: the value of the current - * element and its key. The callback should return a value that can be - * evaluated to boolean. If the callback returns false, the iteration is - * broken. Otherwise, the iteration continues. + * Select only certain keys from a single-dimensional array. * - * @param array $array The array to be iterated over. - * @param callable $callback The callback function to apply to each element. + * This method is the single-dimensional equivalent of ArrayMulti::only. * - * @return array The original array. + * @param array $array The array to select from. + * @param array|string $keys The keys to select. + * @return array A new array with the selected keys. */ - public static function each(array $array, callable $callback): array + public static function only(array $array, array|string $keys): array { - foreach ($array as $key => $value) { - if ($callback($value, $key) === false) { - break; - } - } - return $array; + return array_intersect_key($array, array_flip((array)$keys)); } /** - * Reduce an array to a single value using a callback function. + * "Paginate" the array by slicing it into a smaller segment. * - * The callback function should accept three arguments: the accumulator, - * the current array value, and the current array key. It should return - * the updated accumulator value. + * @param array $array The array to paginate. + * @param int $page The page number to retrieve (1-indexed). + * @param int $perPage The number of items per page. * - * @param array $array The array to reduce. - * @param callable $callback The callback function to apply to each element. - * @param mixed $initial The initial value of the accumulator. - * @return mixed The reduced value. + * @return array The paginated slice of the array. */ - public static function reduce(array $array, callable $callback, mixed $initial = null): mixed + public static function paginate(array $array, int $page, int $perPage): array { - $accumulator = $initial; - foreach ($array as $key => $value) { - $accumulator = $callback($accumulator, $value, $key); - } - return $accumulator; + return array_slice( + $array, + max(0, ($page - 1) * $perPage), + $perPage, + true, + ); } /** - * Determine if at least one element in the array passes the given truth test. + * Partition the array into two arrays [passed, failed] based on a callback. * - * @param array $array The array to search. - * @param callable $callback The callback to apply to each element. - * @return bool Whether at least one element passed the truth test. - */ - public static function some(array $array, callable $callback): bool - { - foreach ($array as $key => $value) { - if ($callback($value, $key)) { - return true; - } - } - return false; - } - - /** - * Determine if all elements in the array pass the given truth test. + * The method takes an array and a callback as parameters. + * It iterates over the array, applying the callback to each item. + * If the callback returns true, the item is added to the "passed" array. + * If the callback returns false, the item is added to the "failed" array. + * The method returns an array with two elements, the first being the "passed" array, + * and the second being the "failed" array. * - * @param array $array The array to search. - * @param callable $callback The callback to apply to each element. - * @return bool Whether all elements passed the truth test. + * @param array $array The array to partition. + * @param callable $callback The callback to use for partitioning. + * @return array An array with two elements, the first being the "passed" array, and the second being the "failed" array. */ - public static function every(array $array, callable $callback): bool + public static function partition(array $array, callable $callback): array { + $passed = []; + $failed = []; + foreach ($array as $key => $value) { - if (!$callback($value, $key)) { - return false; + if ($callback($value, $key)) { + $passed[$key] = $value; + } else { + $failed[$key] = $value; } } - return true; + return [$passed, $failed]; } /** - * Determine if the array contains a given value or if a callback function - * returns true for at least one element. - * - * If the second argument is a callable, it is used as a callback function - * that receives the value and key of each element in the array. If the - * callback returns true, the function returns true. - * - * If the second argument is not a callable, it is used as the value to - * search for in the array. The optional third argument determines whether - * to use strict comparison (===) or loose comparison (==). + * Get only the positive numeric values from the array. * - * @param array $array The array to search. - * @param mixed $valueOrCallback The value to search for, or a callable to apply to each element. - * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). - * @return bool Whether the array contains the given value or whether the callback returned true for at least one element. + * @param array $array The array to check. + * @return array The positive numeric values. */ - public static function contains(array $array, mixed $valueOrCallback, bool $strict = false): bool + public static function positive(array $array): array { - if (is_callable($valueOrCallback)) { - return static::some($array, $valueOrCallback); - } - return in_array($valueOrCallback, $array, $strict); + return static::where($array, static fn ($value) => is_numeric($value) && $value > 0); } /** - * Return the sum of all the elements in the array. + * Prepend a value to the beginning of an array. * - * If a callback is provided, it will be executed for each element in the - * array and the return value will be added to the total. + * If the second parameter is null, the value is prepended as the first element + * in the array. If the second parameter is a key, the value is prepended with + * that key. * - * @param array $array The array to sum. - * @param callable|null $callback The callback to execute for each element. - * @return float|int The sum of all the elements in the array. + * @param array $array The array to prepend to. + * @param mixed $value The value to prepend. + * @param mixed $key The key to prepend with. If null, the value is prepended as the first element. + * @return array The modified array. */ - public static function sum(array $array, ?callable $callback = null): float|int + public static function prepend(array $array, mixed $value, mixed $key = null): array { - if ($callback === null) { - return array_sum($array); - } - - $total = 0; - foreach ($array as $value) { - $total += $callback($value); + if ($key === null) { + array_unshift($array, $value); + } else { + $array = [$key => $value] + $array; } - return $total; + return $array; } /** - * Return an array with all duplicate values removed. - * - * The second parameter, $strict, determines whether to use strict comparison (===) or loose comparison (==) when - * checking for duplicate values. If not provided, it defaults to false, which means loose comparison will be used. + * Reduce an array to a single value using a callback function. * - * The method returns an array with the same type of indices as the input array. + * The callback function should accept three arguments: the accumulator, + * the current array value, and the current array key. It should return + * the updated accumulator value. * - * @param array $array The array to remove duplicates from. - * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). Defaults to false. - * @return array The array with all duplicate values removed. + * @param array $array The array to reduce. + * @param callable $callback The callback function to apply to each element. + * @param mixed $initial The initial value of the accumulator. + * @return mixed The reduced value. */ - public static function unique(array $array, bool $strict = false): array + public static function reduce(array $array, callable $callback, mixed $initial = null): mixed { - if (!$strict) { - return array_values(array_unique($array)); - } - // Manual strict approach: - $checked = []; - $result = []; - foreach ($array as $item) { - if (!in_array($item, $checked, true)) { - $checked[] = $item; - $result[] = $item; - } + $accumulator = $initial; + foreach ($array as $key => $value) { + $accumulator = $callback($accumulator, $value, $key); } - return $result; + return $accumulator; } /** @@ -585,23 +506,76 @@ public static function reject(array $array, mixed $callback = true): array } /** - * Return a slice of the array, starting from the given offset and with the given length. + * Search the array for a given value and return its key if found. * - * The method takes three parameters: the array to slice, the offset from which to start the slice, - * and the length of the slice. If the length is not provided, the method will return all elements - * starting from the given offset. + * If the value is a callable, it will be called for each element in the array, + * and if the callback returns true, the key will be returned. If the value is + * not a callable, the function will search for the value in the array using + * strict comparison. If the value is found, its key will be returned. If the + * value is not found, null will be returned. * - * The method returns an array with the same type of indices as the input array. + * @param array $array The array to search. + * @param mixed $needle The value to search for, or a callable to use for + * searching. * - * @param array $array The array to slice. - * @param int $offset The offset from which to start the slice. - * @param int|null $length The length of the slice. If not provided, the method will return all elements - * starting from the given offset. - * @return array The sliced array. + * @return int|string|null The key of the value if found, or null if not found. */ - public static function slice(array $array, int $offset, ?int $length = null): array + public static function search(array $array, mixed $needle): int|string|null { - return array_slice($array, $offset, $length, true); + if (is_callable($needle)) { + foreach ($array as $key => $value) { + if ($needle($value, $key) === true) { + return $key; + } + } + return null; + } + $foundKey = array_search($needle, $array, true); + return $foundKey === false ? null : $foundKey; + } + + /** + * Split an array into separate arrays of keys and values. + * + * Useful for destructuring an array into separate key and value arrays. + * + * @param array $array The array to split. + * @return array A new array containing two child arrays: 'keys' and 'values'. + * @example + * $data = ['a' => 1, 'b' => 2, 'c' => 3]; + * $keysAndValues = ArraySingle::separate($data); + * // $keysAndValues === ['keys' => ['a', 'b', 'c'], 'values' => [1, 2, 3]]; + */ + public static function separate(array $array): array + { + return [ + 'keys' => array_keys($array), + 'values' => array_values($array), + ]; + } + + /** + * Randomly shuffles the elements in the given array. + * + * If no seed is given, the internal PHP random number generator is used. + * If a seed is given, the Mersenne Twister random number generator is + * seeded with the given value, used to shuffle the array, and then reset + * to the current internal PHP random number generator seed. + * + * @param array $array The array to shuffle. + * @param int|null $seed Optional seed for the Mersenne Twister. + * @return array The shuffled array. + */ + public static function shuffle(array $array, ?int $seed = null): array + { + if ($seed === null) { + \shuffle($array); + } else { + \mt_srand($seed); + \shuffle($array); + \mt_srand(); + } + return $array; } /** @@ -618,6 +592,25 @@ public static function skip(array $array, int $count): array return static::slice($array, $count); } + /** + * Skip items until the callback returns true, then keep the remainder. + * + * The method takes an array and a callback as parameters. + * It iterates over the array, applying the callback to each item. + * As long as the callback returns false, the item is skipped. + * The first item for which the callback returns true is kept, + * and all subsequent items are also kept. + * The method returns an array with the same type of indices as the input array. + * + * @param array $array The array to skip. + * @param callable $callback The callback to use for skipping. + * @return array The skipped array. + */ + public static function skipUntil(array $array, callable $callback): array + { + return static::skipWhile($array, fn ($value, $key) => !$callback($value, $key)); + } + /** * Skip items while the callback returns true; once false, keep the remainder. * @@ -649,106 +642,111 @@ public static function skipWhile(array $array, callable $callback): array } /** - * Skip items until the callback returns true, then keep the remainder. + * Return a slice of the array, starting from the given offset and with the given length. + * + * The method takes three parameters: the array to slice, the offset from which to start the slice, + * and the length of the slice. If the length is not provided, the method will return all elements + * starting from the given offset. * - * The method takes an array and a callback as parameters. - * It iterates over the array, applying the callback to each item. - * As long as the callback returns false, the item is skipped. - * The first item for which the callback returns true is kept, - * and all subsequent items are also kept. * The method returns an array with the same type of indices as the input array. * - * @param array $array The array to skip. - * @param callable $callback The callback to use for skipping. - * @return array The skipped array. + * @param array $array The array to slice. + * @param int $offset The offset from which to start the slice. + * @param int|null $length The length of the slice. If not provided, the method will return all elements + * starting from the given offset. + * @return array The sliced array. */ - public static function skipUntil(array $array, callable $callback): array + public static function slice(array $array, int $offset, ?int $length = null): array { - return static::skipWhile($array, fn ($value, $key) => !$callback($value, $key)); + return array_slice($array, $offset, $length, true); } /** - * Partition the array into two arrays [passed, failed] based on a callback. - * - * The method takes an array and a callback as parameters. - * It iterates over the array, applying the callback to each item. - * If the callback returns true, the item is added to the "passed" array. - * If the callback returns false, the item is added to the "failed" array. - * The method returns an array with two elements, the first being the "passed" array, - * and the second being the "failed" array. + * Determine if at least one element in the array passes the given truth test. * - * @param array $array The array to partition. - * @param callable $callback The callback to use for partitioning. - * @return array An array with two elements, the first being the "passed" array, and the second being the "failed" array. + * @param array $array The array to search. + * @param callable $callback The callback to apply to each element. + * @return bool Whether at least one element passed the truth test. */ - public static function partition(array $array, callable $callback): array + public static function some(array $array, callable $callback): bool { - $passed = []; - $failed = []; - foreach ($array as $key => $value) { if ($callback($value, $key)) { - $passed[$key] = $value; - } else { - $failed[$key] = $value; + return true; } } - return [$passed, $failed]; + return false; } /** - * Find the mode(s) of the array. + * Return the sum of all the elements in the array. * - * The mode is the value that appears most frequently in the array. - * If there are multiple modes, all of them are returned. + * If a callback is provided, it will be executed for each element in the + * array and the return value will be added to the total. * - * @param array $array The array to find the mode(s) of. - * @return array The mode(s) of the array. + * @param array $array The array to sum. + * @param callable|null $callback The callback to execute for each element. + * @return float|int The sum of all the elements in the array. */ - public static function mode(array $array): array + public static function sum(array $array, ?callable $callback = null): float|int { - if ($array === []) { - return []; + if ($callback === null) { + return array_sum($array); } - $freq = array_count_values($array); - $max = max($freq); - return array_keys(array_filter($freq, fn ($c) => $c === $max)); + + $total = 0; + foreach ($array as $value) { + $total += $callback($value); + } + return $total; } /** - * Calculate the median of an array of numbers. + * Return an array with all duplicate values removed. * - * The median is the middle value in a sorted list of numbers. If the list has an - * odd number of elements, the median is the element at the middle index. If the list - * has an even number of elements, the median is the average of the two middle elements. + * The second parameter, $strict, determines whether to use strict comparison (===) or loose comparison (==) when + * checking for duplicate values. If not provided, it defaults to false, which means loose comparison will be used. * - * @param array $array The array of numbers to find the median of. - * @return float|int The median of the numbers in the array. If the array is empty, 0 is returned. + * The method returns an array with the same type of indices as the input array. + * + * @param array $array The array to remove duplicates from. + * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). Defaults to false. + * @return array The array with all duplicate values removed. */ - public static function median(array $array): float|int + public static function unique(array $array, bool $strict = false): array { - if ($array === []) { - return 0; + if (!$strict) { + return array_values(array_unique($array)); } - $values = $array; - sort($values, SORT_NUMERIC); - $count = count($values); - $mid = intdiv($count, 2); - - return ($count % 2) - ? $values[$mid] - : ($values[$mid - 1] + $values[$mid]) / 2; + // Manual strict approach: + $checked = []; + $result = []; + foreach ($array as $item) { + if (!in_array($item, $checked, true)) { + $checked[] = $item; + $result[] = $item; + } + } + return $result; } /** - * Get all items from the array except for those with the specified keys. + * Filter the array using a callback function. * - * @param array $array The array to select from. - * @param array|string $keys The keys to exclude. - * @return array A new array with all items except for those with the specified keys. + * If the callback is omitted, the function will return all elements in the + * array that are truthy. + * + * @param array $array The array to search. + * @param callable|null $callback The callback function to use for filtering. + * This function should take two arguments, the value and the key of each + * element in the array. The function should return true for elements that + * should be kept, and false for elements that should be discarded. + * + * @return array The filtered array. */ - public static function except(array $array, array|string $keys): array + public static function where(array $array, ?callable $callback = null): array { - return array_diff_key($array, array_flip((array) $keys)); + $flag = ($callback !== null) ? \ARRAY_FILTER_USE_BOTH : 0; + return array_filter($array, $callback ?? fn ($val) => (bool)$val, $flag); } } diff --git a/src/Array/BaseArrayHelper.php b/src/Array/BaseArrayHelper.php index 80dd67f..895f78d 100644 --- a/src/Array/BaseArrayHelper.php +++ b/src/Array/BaseArrayHelper.php @@ -10,92 +10,74 @@ class BaseArrayHelper { /** - * Check if an array is multi-dimensional. - * - * This method is a shortcut for checking if an array is multi-dimensional. - * It checks if the array is an array and if the count of the array is not - * equal to the count of the array with the COUNT_RECURSIVE flag. + * Check if the given value is an array or an instance of ArrayAccess. * - * @param mixed $array The array to check. - * @return bool True if the array is multi-dimensional, false otherwise. + * @param mixed $value The value to check. + * @return bool True if the value is accessible, false otherwise. */ - public static function isMultiDimensional(mixed $array): bool + public static function accessible(mixed $value): bool { - return is_array($array) - && count($array) !== count($array, COUNT_RECURSIVE); + return is_array($value) || $value instanceof ArrayAccess; } /** - * Wrap a value in an array if it's not already an array; otherwise return the array as is. + * Check if all elements in the array pass the given truth test. * - * If the value is empty, an empty array is returned. + * This function applies a callback to each element of the array. + * If the callback returns true for all elements, the function returns true. + * Otherwise, it returns false. * - * @param mixed $value The value to wrap. - * @return array The wrapped value. + * @param array $array The array to be evaluated. + * @param callable $callback The callback function to apply to each element. + * @return bool True if all elements pass the truth test, false otherwise. */ - public static function wrap(mixed $value): array + public static function all(array $array, callable $callback): bool { - if (empty($value)) { - return []; - } - return is_array($value) ? $value : [$value]; + return static::isAll($array, $callback); } /** - * Unwrap a value from an array if it contains exactly one element. + * Check if at least one element in the array passes a given truth test. * - * This method checks if the given value is an array. If it is not, - * the value is returned as is. If the value is an array with exactly - * one element, that element is returned. Otherwise, the array itself - * is returned. + * This function is an alias for haveAny, which is a more descriptive name. + * It is provided for syntactic sugar, as it is very common to want to + * check if at least one item in an array matches a given criteria. * - * @param mixed $value The value to potentially unwrap. - * @return mixed The unwrapped value or the original array. + * @param array $array The array to check. + * @param callable $callback The callback to apply to each element. + * @return bool True if at least one element passes the test, false otherwise. */ - public static function unWrap(mixed $value): mixed + public static function any(array $array, callable $callback): bool { - if (!is_array($value)) { - return $value; - } - return (count($value) === 1) ? reset($value) : $value; + return static::haveAny($array, $callback); } /** - * Determine if at least one element in the array passes the given truth test. + * Filter an array by rejecting elements based on a callback function or value. * - * @param array $array The array to search. - * @param callable $callback The callback to use for searching. - * @return bool Whether at least one element passed the truth test. - */ - public static function haveAny(array $array, callable $callback): bool - { - foreach ($array as $key => $value) { - if ($callback($value, $key) === true) { - return true; - } - } - return false; - } - - - /** - * Determine if all elements in the array pass the given truth test. + * This function takes an array and a callback or value as parameters. + * If the callback is callable, it applies the callback to each element of the array. + * Elements for which the callback returns false are kept. + * If the callback is a value, elements equal to this value are rejected. + * The function returns an array with the same type of indices as the input array. * - * @param array $array The array to search. - * @param callable $callback The callback to use for searching. - * @return bool Whether all elements passed the truth test. + * @param array $array The array to be filtered. + * @param mixed $callback The callback function or value for filtering. + * @return array The array with elements rejected based on the callback or value. */ - public static function isAll(array $array, callable $callback): bool + public static function doReject(array $array, mixed $callback): array { - foreach ($array as $key => $value) { - if ($callback($value, $key) === false) { - return false; - } + if (is_callable($callback)) { + return array_filter( + $array, + fn ($val, $key) => !$callback($val, $key), + ARRAY_FILTER_USE_BOTH + ); } - return true; + return array_filter($array, fn ($val) => $val != $callback); } @@ -119,14 +101,20 @@ public static function findKey(array $array, callable $callback): int|string|nul /** - * Check if the given value is an array or an instance of ArrayAccess. + * Remove one or multiple array items from an array. * - * @param mixed $value The value to check. - * @return bool True if the value is accessible, false otherwise. + * This function takes an array and a key or array of keys as parameters. + * It then iterates over the given keys, and unsets the corresponding + * items from the array. + * + * @param array $array The array from which to remove items. + * @param int|string|array $keys The key or array of keys to be removed. */ - public static function accessible(mixed $value): bool + public static function forget(array &$array, int|string|array $keys): void { - return is_array($value) || $value instanceof ArrayAccess; + foreach ((array) $keys as $key) { + unset($array[$key]); + } } @@ -183,87 +171,115 @@ public static function hasAny(array $array, int|string|array $keys): bool /** - * Generate an array containing a sequence of numbers. - * - * This function creates an array of numbers starting from $start up to $end, - * incrementing by $step. If $step is zero, an empty array is returned. + * Determine if at least one element in the array passes the given truth test. * - * @param int $start The starting number of the sequence. - * @param int $end The ending number of the sequence. - * @param int $step The increment between each number in the sequence. Defaults to 1. - * @return array An array containing the sequence of numbers. + * @param array $array The array to search. + * @param callable $callback The callback to use for searching. + * @return bool Whether at least one element passed the truth test. */ - public static function range(int $start, int $end, int $step = 1): array + public static function haveAny(array $array, callable $callback): bool { - if ($step === 0) { - // We could throw an exception, or return empty: - return []; + foreach ($array as $key => $value) { + if ($callback($value, $key) === true) { + return true; + } } - return range($start, $end, $step); + return false; } /** - * Create an array of the specified length and fill it with the results of the - * given callback function. If the callback is not provided, the array will be - * filled with the numbers 1 through $number. - * - * Example: - * ArrayKit::times(3, function ($i) { - * return "Row #{$i}"; - * }); - * // Output: ["Row #1", "Row #2", "Row #3"] + * Determine if all elements in the array pass the given truth test. * - * @param int $number The length of the array. - * @param callable|null $callback The callback function to use. - * @return array The filled array. + * @param array $array The array to search. + * @param callable $callback The callback to use for searching. + * @return bool Whether all elements passed the truth test. */ - public static function times(int $number, ?callable $callback = null): array + public static function isAll(array $array, callable $callback): bool { - $results = []; - if ($number < 1) { - return $results; - } - - for ($i = 1; $i <= $number; $i++) { - $results[] = $callback ? $callback($i) : $i; + foreach ($array as $key => $value) { + if ($callback($value, $key) === false) { + return false; + } } - - return $results; + return true; + } + /** + * Check if an array is multi-dimensional. + * + * This method is a shortcut for checking if an array is multi-dimensional. + * It checks if the array is an array and if the count of the array is not + * equal to the count of the array with the COUNT_RECURSIVE flag. + * + * @param mixed $array The array to check. + * @return bool True if the array is multi-dimensional, false otherwise. + */ + public static function isMultiDimensional(mixed $array): bool + { + return is_array($array) + && count($array) !== count($array, COUNT_RECURSIVE); } /** - * Check if at least one element in the array passes a given truth test. + * Retrieve one or multiple random items from an array. * - * This function is an alias for haveAny, which is a more descriptive name. - * It is provided for syntactic sugar, as it is very common to want to - * check if at least one item in an array matches a given criteria. + * By default, this function returns a single item from the array. + * If you pass a number as the second argument, it will return that + * number of items. If you set the third argument to `true`, the + * keys from the original array are preserved in the returned array. * - * @param array $array The array to check. - * @param callable $callback The callback to apply to each element. - * @return bool True if at least one element passes the test, false otherwise. + * @param array $array The array from which to retrieve random items. + * @param int|null $number The number of items to retrieve. If null, a single item is returned. + * @param bool $preserveKeys Whether to preserve the keys from the original array. + * + * @return mixed The retrieved item(s) from the array. + * + * @throws InvalidArgumentException If the user requested more items than the array contains. */ - public static function any(array $array, callable $callback): bool + public static function random(array $array, ?int $number = null, bool $preserveKeys = false): mixed { - return static::haveAny($array, $callback); + $count = count($array); + if ($count === 0 || ($number !== null && $number <= 0)) { + return ($number === null) ? null : []; + } + + if ($number === null) { + $randKey = array_rand($array); + return $array[$randKey]; + } + + if ($number > $count) { + throw new InvalidArgumentException("You requested $number items, but array only has $count."); + } + + $keys = (array) array_rand($array, $number); + + // intersect is ~30 % faster than manual loop for large n + $picked = array_intersect_key($array, array_flip($keys)); + + return $preserveKeys ? $picked : array_values($picked); } /** - * Check if all elements in the array pass the given truth test. + * Generate an array containing a sequence of numbers. * - * This function applies a callback to each element of the array. - * If the callback returns true for all elements, the function returns true. - * Otherwise, it returns false. + * This function creates an array of numbers starting from $start up to $end, + * incrementing by $step. If $step is zero, an empty array is returned. * - * @param array $array The array to be evaluated. - * @param callable $callback The callback function to apply to each element. - * @return bool True if all elements pass the truth test, false otherwise. + * @param int $start The starting number of the sequence. + * @param int $end The ending number of the sequence. + * @param int $step The increment between each number in the sequence. Defaults to 1. + * @return array An array containing the sequence of numbers. */ - public static function all(array $array, callable $callback): bool + public static function range(int $start, int $end, int $step = 1): array { - return static::isAll($array, $callback); + if ($step === 0) { + // We could throw an exception, or return empty: + return []; + } + return range($start, $end, $step); } /* ------------------------------------------------------------------------ @@ -288,86 +304,68 @@ public static function tap(array $array, callable $callback): array /** - * Remove one or multiple array items from an array. + * Create an array of the specified length and fill it with the results of the + * given callback function. If the callback is not provided, the array will be + * filled with the numbers 1 through $number. * - * This function takes an array and a key or array of keys as parameters. - * It then iterates over the given keys, and unsets the corresponding - * items from the array. + * Example: + * ArrayKit::times(3, function ($i) { + * return "Row #{$i}"; + * }); + * // Output: ["Row #1", "Row #2", "Row #3"] * - * @param array $array The array from which to remove items. - * @param int|string|array $keys The key or array of keys to be removed. + * @param int $number The length of the array. + * @param callable|null $callback The callback function to use. + * @return array The filled array. */ - public static function forget(array &$array, int|string|array $keys): void + public static function times(int $number, ?callable $callback = null): array { - foreach ((array) $keys as $key) { - unset($array[$key]); + $results = []; + if ($number < 1) { + return $results; + } + + for ($i = 1; $i <= $number; $i++) { + $results[] = $callback ? $callback($i) : $i; } + + return $results; } /** - * Retrieve one or multiple random items from an array. - * - * By default, this function returns a single item from the array. - * If you pass a number as the second argument, it will return that - * number of items. If you set the third argument to `true`, the - * keys from the original array are preserved in the returned array. - * - * @param array $array The array from which to retrieve random items. - * @param int|null $number The number of items to retrieve. If null, a single item is returned. - * @param bool $preserveKeys Whether to preserve the keys from the original array. + * Unwrap a value from an array if it contains exactly one element. * - * @return mixed The retrieved item(s) from the array. + * This method checks if the given value is an array. If it is not, + * the value is returned as is. If the value is an array with exactly + * one element, that element is returned. Otherwise, the array itself + * is returned. * - * @throws InvalidArgumentException If the user requested more items than the array contains. + * @param mixed $value The value to potentially unwrap. + * @return mixed The unwrapped value or the original array. */ - public static function random(array $array, int $number = null, bool $preserveKeys = false): mixed + public static function unWrap(mixed $value): mixed { - $count = count($array); - if ($count === 0 || ($number !== null && $number <= 0)) { - return ($number === null) ? null : []; - } - - if ($number === null) { - $randKey = array_rand($array); - return $array[$randKey]; - } - - if ($number > $count) { - throw new InvalidArgumentException("You requested $number items, but array only has $count."); + if (!is_array($value)) { + return $value; } - - $keys = (array) array_rand($array, $number); - - // intersect is ~30 % faster than manual loop for large n - $picked = array_intersect_key($array, array_flip($keys)); - - return $preserveKeys ? $picked : array_values($picked); + return (count($value) === 1) ? reset($value) : $value; } /** - * Filter an array by rejecting elements based on a callback function or value. + * Wrap a value in an array if it's not already an array; otherwise return the array as is. * - * This function takes an array and a callback or value as parameters. - * If the callback is callable, it applies the callback to each element of the array. - * Elements for which the callback returns false are kept. - * If the callback is a value, elements equal to this value are rejected. - * The function returns an array with the same type of indices as the input array. + * If the value is empty, an empty array is returned. * - * @param array $array The array to be filtered. - * @param mixed $callback The callback function or value for filtering. - * @return array The array with elements rejected based on the callback or value. + * @param mixed $value The value to wrap. + * @return array The wrapped value. */ - public static function doReject(array $array, mixed $callback): array + public static function wrap(mixed $value): array { - if (is_callable($callback)) { - return array_filter( - $array, - fn ($val, $key) => !$callback($val, $key), - ARRAY_FILTER_USE_BOTH - ); + if (empty($value)) { + return []; } - return array_filter($array, fn ($val) => $val != $callback); + return is_array($value) ? $value : [$value]; } } diff --git a/src/Array/DotNotation.php b/src/Array/DotNotation.php index af1a2a4..1a9cd37 100644 --- a/src/Array/DotNotation.php +++ b/src/Array/DotNotation.php @@ -9,29 +9,59 @@ class DotNotation { /** - * Flattens a multidimensional array to a single level, using dot notation to - * represent nested keys. + * Get all the given array. + */ + public static function all(array $array): array + { + return $array; + } + + /** + * Retrieve an array value from the array using a dot-notation key. * - * @param array $array The multidimensional array to flatten. - * @param string $prepend A string to prepend to the keys of the flattened array. - * @return array A flattened array with all nested arrays collapsed to the same level. + * This method tries to fetch a value from the given array with the specified key. + * If the value is not an array, an InvalidArgumentException is thrown. + * If the key is not found, the default value is returned. + * + * @param array $array The array to retrieve the value from. + * @param string $key The dot-notation key to use for retrieval. + * @param mixed $default The default value to return if the key is not found. + * @return array The retrieved array value. + * + * @throws InvalidArgumentException If the retrieved value is not an array. */ - public static function flatten(array $array, string $prepend = ''): array + public static function arrayValue(array $array, string $key, mixed $default = null): array { - $results = []; + $value = static::get($array, $key, $default); + if (! is_array($value)) { + throw new InvalidArgumentException('Expected array, got '.get_debug_type($value)); + } - foreach ($array as $key => $value) { - if (is_array($value) && ! empty($value)) { - $results = array_merge( - $results, - static::flatten($value, $prepend.$key.'.') - ); - } else { - $results[$prepend.$key] = $value; - } + return $value; + } + + /** + * Retrieve a boolean value from the array using a dot-notation key. + * + * This method tries to fetch a value from the given array with the specified key. + * If the value is not a boolean, an InvalidArgumentException is thrown. + * If the key is not found, the default value is returned. + * + * @param array $array The array to retrieve the value from. + * @param string $key The dot-notation key to use for retrieval. + * @param mixed $default The default value to return if the key is not found. + * @return bool The retrieved boolean value. + * + * @throws InvalidArgumentException If the retrieved value is not a boolean. + */ + public static function boolean(array $array, string $key, mixed $default = null): bool + { + $value = static::get($array, $key, $default); + if (! is_bool($value)) { + throw new InvalidArgumentException('Expected bool, got '.get_debug_type($value)); } - return $results; + return $value; } /** @@ -53,140 +83,64 @@ public static function expand(array $array): array } /** - * Determine if the given key or keys exist in the array. - * - * This method is the dot notation aware version of ArraySingle::has. + * Fill in data where missing (like set, but doesn't overwrite existing keys). * - * @param array $array The array to search. - * @param array|string $keys The key(s) to check for existence. - * @return bool True if all the given keys exist in the array, false otherwise. + * @param array $array The array to fill in. + * @param array|string $keys The key(s) to fill in. + * @param mixed $value The value to set if missing. */ - public static function has(array $array, array|string $keys): bool + public static function fill(array &$array, array|string $keys, mixed $value = null): void { - if (empty($array) || empty($keys)) { - return false; - } - - // If single string key and found top-level: - if (is_string($keys) && ArraySingle::exists($array, $keys)) { - return true; - } - - $keys = (array) $keys; - foreach ($keys as $key) { - if (ArraySingle::exists($array, $key)) { - continue; - } - // Fall back to a simple segment check (no wildcard) - if (! static::segmentExact($array, $key, false)) { - return false; - } - } - - return true; + static::set($array, $keys, $value, false); } - /** - * Check if *any* of the given keys exist (no wildcard). + * Flattens a multidimensional array to a single level, using dot notation to + * represent nested keys. * - * @param array $array The array to search. - * @param array|string $keys The key(s) to check for existence. - * @return bool True if at least one key exists + * @param array $array The multidimensional array to flatten. + * @param string $prepend A string to prepend to the keys of the flattened array. + * @return array A flattened array with all nested arrays collapsed to the same level. */ - public static function hasAny(array $array, array|string $keys): bool + public static function flatten(array $array, string $prepend = ''): array { - if (empty($array) || empty($keys)) { - return false; - } + $results = []; - $keys = (array) $keys; - foreach ($keys as $key) { - if (static::has($array, $key)) { - return true; + foreach ($array as $key => $value) { + if (is_array($value) && ! empty($value)) { + $results = array_merge( + $results, + static::flatten($value, $prepend.$key.'.') + ); + } else { + $results[$prepend.$key] = $value; } } - return false; + return $results; } /** - * Get one or multiple items from the array using dot notation. + * Retrieve a float value from the array using a dot-notation key. * - * The following cases are handled: - * - If no key is provided, the entire array is returned. - * - If an array of keys is provided, all values are returned in an array. - * - If a single key is provided, the value is returned directly. + * This method tries to fetch a value from the given array with the specified key. + * If the value is not a float, an InvalidArgumentException is thrown. + * If the key is not found, the default value is returned. * - * @param array $array The array to retrieve items from. - * @param array|int|string|null $keys The key(s) to retrieve. + * @param array $array The array to retrieve the value from. + * @param string $key The dot-notation key to use for retrieval. * @param mixed $default The default value to return if the key is not found. - * @return mixed The retrieved value(s). - */ - public static function get(array $array, array|int|string|null $keys = null, mixed $default = null): mixed - { - // If no key, return entire array - if ($keys === null) { - return $array; - } - - // If multiple keys requested, gather each value: - if (is_array($keys)) { - $results = []; - foreach ($keys as $k) { - $results[$k] = static::getValue($array, $k, $default); - } - - return $results; - } - - // single key - return static::getValue($array, $keys, $default); - } - - /** - * Set one or multiple items in the array using dot notation. - * - * If no key is provided, the entire array is replaced with $value. - * If an array of key-value pairs is provided, each value is set. - * If a single key is provided, the value is set directly. + * @return float The retrieved float value. * - * @param array $array The array to set items in. - * @param array|string|null $keys The key(s) to set. - * @param mixed $value The value to set. - * @param bool $overwrite If true, existing values are overwritten. If false, existing values are preserved. - * @return bool True on success + * @throws InvalidArgumentException If the retrieved value is not a float. */ - public static function set(array &$array, array|string|null $keys = null, mixed $value = null, bool $overwrite = true): bool + public static function float(array $array, string $key, mixed $default = null): float { - // If no key, replace entire array with $value - if ($keys === null) { - $array = (array) $value; - - return true; - } - - if (is_array($keys)) { - // multiple sets - foreach ($keys as $k => $val) { - static::setValue($array, $k, $val, $overwrite); - } - } else { - static::setValue($array, $keys, $value, $overwrite); + $value = static::get($array, $key, $default); + if (! is_float($value)) { + throw new InvalidArgumentException('Expected float, got '.get_debug_type($value)); } - return true; - } - - /** - * Fill in data where missing (like set, but doesn't overwrite existing keys). - * - * @param array $array The array to fill in. - * @param array|string $keys The key(s) to fill in. - * @param mixed $value The value to set if missing. - */ - public static function fill(array &$array, array|string $keys, mixed $value = null): void - { - static::set($array, $keys, $value, false); + return $value; } /** @@ -229,202 +183,118 @@ public static function forget(array &$target, array|string|int|null $keys): void } /** - * Recursively apply the forget logic to each element in an array. - * - * This function iterates over each element of the provided array - * and applies the forget operation using the given segments. - * - * @param array $array The array whose elements will be processed. - * @param array $segments The segments to use for the forget operation. - */ - private static function forgetEach(array &$array, array $segments): void - { - foreach ($array as &$inner) { - static::forget($inner, $segments); - } - } - - /** - * Unset a property from an object. - * - * This method removes a specified property from an object by using - * PHP's unset function. The property is directly removed from the - * object if it exists. - * - * @param object $object The object from which the property should be removed. - * @param string $property The name of the property to unset. - */ - private static function unsetProperty(object &$object, string $property): void - { - unset($object->{$property}); - } - - /** - * Retrieve a string value from the array using a dot-notation key. + * Get one or multiple items from the array using dot notation. * - * This function attempts to retrieve a value from the given array - * using the specified key. If the retrieved value is not of type - * string, an InvalidArgumentException is thrown. If the key is not - * found, the default value is returned. + * The following cases are handled: + * - If no key is provided, the entire array is returned. + * - If an array of keys is provided, all values are returned in an array. + * - If a single key is provided, the value is returned directly. * - * @param array $array The array to retrieve the value from. - * @param string $key The dot-notation key to use for retrieval. + * @param array $array The array to retrieve items from. + * @param array|int|string|null $keys The key(s) to retrieve. * @param mixed $default The default value to return if the key is not found. - * @return string The retrieved string value. - * - * @throws InvalidArgumentException If the retrieved value is not a string. + * @return mixed The retrieved value(s). */ - public static function string(array $array, string $key, mixed $default = null): string + public static function get(array $array, array|int|string|null $keys = null, mixed $default = null): mixed { - $value = static::get($array, $key, $default); - if (! is_string($value)) { - throw new InvalidArgumentException('Expected string, got '.get_debug_type($value)); + // If no key, return entire array + if ($keys === null) { + return $array; } - return $value; - } + // If multiple keys requested, gather each value: + if (is_array($keys)) { + $results = []; + foreach ($keys as $k) { + $results[$k] = static::getValue($array, $k, $default); + } - /** - * Retrieve an integer value from the array using a dot-notation key. - * - * This method tries to fetch a value from the given array with the specified key. - * If the value is not an integer, an InvalidArgumentException is thrown. - * If the key is not found, the default value is returned. - * - * @param array $array The array to retrieve the value from. - * @param string $key The dot-notation key to use for retrieval. - * @param mixed $default The default value to return if the key is not found. - * @return int The retrieved integer value. - * - * @throws InvalidArgumentException If the retrieved value is not an integer. - */ - public static function integer(array $array, string $key, mixed $default = null): int - { - $value = static::get($array, $key, $default); - if (! is_int($value)) { - throw new InvalidArgumentException('Expected int, got '.get_debug_type($value)); + return $results; } - return $value; + // single key + return static::getValue($array, $keys, $default); } /** - * Retrieve a float value from the array using a dot-notation key. - * - * This method tries to fetch a value from the given array with the specified key. - * If the value is not a float, an InvalidArgumentException is thrown. - * If the key is not found, the default value is returned. + * Determine if the given key or keys exist in the array. * - * @param array $array The array to retrieve the value from. - * @param string $key The dot-notation key to use for retrieval. - * @param mixed $default The default value to return if the key is not found. - * @return float The retrieved float value. + * This method is the dot notation aware version of ArraySingle::has. * - * @throws InvalidArgumentException If the retrieved value is not a float. + * @param array $array The array to search. + * @param array|string $keys The key(s) to check for existence. + * @return bool True if all the given keys exist in the array, false otherwise. */ - public static function float(array $array, string $key, mixed $default = null): float + public static function has(array $array, array|string $keys): bool { - $value = static::get($array, $key, $default); - if (! is_float($value)) { - throw new InvalidArgumentException('Expected float, got '.get_debug_type($value)); + if (empty($array) || empty($keys)) { + return false; } - return $value; - } - - /** - * Retrieve a boolean value from the array using a dot-notation key. - * - * This method tries to fetch a value from the given array with the specified key. - * If the value is not a boolean, an InvalidArgumentException is thrown. - * If the key is not found, the default value is returned. - * - * @param array $array The array to retrieve the value from. - * @param string $key The dot-notation key to use for retrieval. - * @param mixed $default The default value to return if the key is not found. - * @return bool The retrieved boolean value. - * - * @throws InvalidArgumentException If the retrieved value is not a boolean. - */ - public static function boolean(array $array, string $key, mixed $default = null): bool - { - $value = static::get($array, $key, $default); - if (! is_bool($value)) { - throw new InvalidArgumentException('Expected bool, got '.get_debug_type($value)); + // If single string key and found top-level: + if (is_string($keys) && ArraySingle::exists($array, $keys)) { + return true; } - return $value; - } - - /** - * Retrieve an array value from the array using a dot-notation key. - * - * This method tries to fetch a value from the given array with the specified key. - * If the value is not an array, an InvalidArgumentException is thrown. - * If the key is not found, the default value is returned. - * - * @param array $array The array to retrieve the value from. - * @param string $key The dot-notation key to use for retrieval. - * @param mixed $default The default value to return if the key is not found. - * @return array The retrieved array value. - * - * @throws InvalidArgumentException If the retrieved value is not an array. - */ - public static function arrayValue(array $array, string $key, mixed $default = null): array - { - $value = static::get($array, $key, $default); - if (! is_array($value)) { - throw new InvalidArgumentException('Expected array, got '.get_debug_type($value)); + $keys = (array) $keys; + foreach ($keys as $key) { + if (ArraySingle::exists($array, $key)) { + continue; + } + // Fall back to a simple segment check (no wildcard) + if (! static::segmentExact($array, $key, false)) { + return false; + } } - return $value; + return true; } /** - * Pluck one or more values from an array. - * - * This method allows you to retrieve one or more values from an array - * using dot-notation keys. + * Check if *any* of the given keys exist (no wildcard). * - * @param array $array The array to retrieve values from. - * @param array|string $keys The key(s) to retrieve. - * @param mixed $default The default value to return if the key is not found. - * @return array The retrieved values. + * @param array $array The array to search. + * @param array|string $keys The key(s) to check for existence. + * @return bool True if at least one key exists */ - public static function pluck(array $array, array|string $keys, mixed $default = null): array + public static function hasAny(array $array, array|string $keys): bool { - $keys = (array) $keys; - $results = []; + if (empty($array) || empty($keys)) { + return false; + } + $keys = (array) $keys; foreach ($keys as $key) { - $results[$key] = static::get($array, $key, $default); + if (static::has($array, $key)) { + return true; + } } - return $results; - } - - /** - * Get all the given array. - */ - public static function all(array $array): array - { - return $array; + return false; } /** - * Pass the array to the given callback and return it. + * Retrieve an integer value from the array using a dot-notation key. * - * Useful for tapping into a fluent method chain for debugging. + * This method tries to fetch a value from the given array with the specified key. + * If the value is not an integer, an InvalidArgumentException is thrown. + * If the key is not found, the default value is returned. * - * @param array $array The array to be tapped. - * @param callable $callback The callback to apply to the array. - * @return array The original array. + * @param array $array The array to retrieve the value from. + * @param string $key The dot-notation key to use for retrieval. + * @param mixed $default The default value to return if the key is not found. + * @return int The retrieved integer value. + * + * @throws InvalidArgumentException If the retrieved value is not an integer. */ - public static function tap(array $array, callable $callback): array + public static function integer(array $array, string $key, mixed $default = null): int { - $callback($array); + $value = static::get($array, $key, $default); + if (! is_int($value)) { + throw new InvalidArgumentException('Expected int, got '.get_debug_type($value)); + } - return $array; + return $value; } /** @@ -491,6 +361,142 @@ public static function offsetUnset(array &$array, string $key): void static::forget($array, $key); } + /** + * Pluck one or more values from an array. + * + * This method allows you to retrieve one or more values from an array + * using dot-notation keys. + * + * @param array $array The array to retrieve values from. + * @param array|string $keys The key(s) to retrieve. + * @param mixed $default The default value to return if the key is not found. + * @return array The retrieved values. + */ + public static function pluck(array $array, array|string $keys, mixed $default = null): array + { + $keys = (array) $keys; + $results = []; + + foreach ($keys as $key) { + $results[$key] = static::get($array, $key, $default); + } + + return $results; + } + + /** + * Set one or multiple items in the array using dot notation. + * + * If no key is provided, the entire array is replaced with $value. + * If an array of key-value pairs is provided, each value is set. + * If a single key is provided, the value is set directly. + * + * @param array $array The array to set items in. + * @param array|string|null $keys The key(s) to set. + * @param mixed $value The value to set. + * @param bool $overwrite If true, existing values are overwritten. If false, existing values are preserved. + * @return bool True on success + */ + public static function set(array &$array, array|string|null $keys = null, mixed $value = null, bool $overwrite = true): bool + { + // If no key, replace entire array with $value + if ($keys === null) { + $array = (array) $value; + + return true; + } + + if (is_array($keys)) { + // multiple sets + foreach ($keys as $k => $val) { + static::setValue($array, $k, $val, $overwrite); + } + } else { + static::setValue($array, $keys, $value, $overwrite); + } + + return true; + } + + /** + * Retrieve a string value from the array using a dot-notation key. + * + * This function attempts to retrieve a value from the given array + * using the specified key. If the retrieved value is not of type + * string, an InvalidArgumentException is thrown. If the key is not + * found, the default value is returned. + * + * @param array $array The array to retrieve the value from. + * @param string $key The dot-notation key to use for retrieval. + * @param mixed $default The default value to return if the key is not found. + * @return string The retrieved string value. + * + * @throws InvalidArgumentException If the retrieved value is not a string. + */ + public static function string(array $array, string $key, mixed $default = null): string + { + $value = static::get($array, $key, $default); + if (! is_string($value)) { + throw new InvalidArgumentException('Expected string, got '.get_debug_type($value)); + } + + return $value; + } + + /** + * Pass the array to the given callback and return it. + * + * Useful for tapping into a fluent method chain for debugging. + * + * @param array $array The array to be tapped. + * @param callable $callback The callback to apply to the array. + * @return array The original array. + */ + public static function tap(array $array, callable $callback): array + { + $callback($array); + + return $array; + } + + /** + * Access a segment in a target array or object. + * + * This method takes a target array or object, a segment, and a default value. + * It returns the value of the segment in the target if it exists, or the default + * value if it does not. It supports both array and object access. Array access + * is attempted first, then object access. + * + * @param mixed $target The target array or object to access. + * @param mixed $segment The segment to access. + * @param mixed $default The default value to return if the segment does not exist. + * @return mixed The value of the segment in the target, or the default value. + */ + private static function accessSegment(mixed $target, mixed $segment, mixed $default): mixed + { + return match (true) { + BaseArrayHelper::accessible($target) && ArraySingle::exists($target, $segment) => $target[$segment], + is_object($target) && isset($target->{$segment}) => $target->{$segment}, + default => static::value($default), + }; + } + + /** + * Recursively apply the forget logic to each element in an array. + * + * This function iterates over each element of the provided array + * and applies the forget operation using the given segments. + * + * @param array $array The array whose elements will be processed. + * @param array $segments The segments to use for the forget operation. + */ + private static function forgetEach(array &$array, array $segments): void + { + foreach ($array as &$inner) { + static::forget($inner, $segments); + } + } + /** * Retrieve a value from the array using dot notation. * @@ -519,39 +525,33 @@ private static function getValue(mixed $target, int|string $key, mixed $default) } /** - * Traverses the target array/object to retrieve a value using dot notation. + * Sets values in the target using dot-notation with wildcard support. * - * This method is called recursively by the `get` method to traverse the given - * array or object using dot notation. It expects the target array or object, - * the segments of the dot-notation key, and the default value to return if - * the key is not found. + * This method handles cases where the first segment in the dot-notation key + * is a wildcard ('*'). It iterates over each element of the target, applying + * the remaining segments to set the specified value. If segments are present, + * it continues setting values recursively. If the overwrite flag is true and + * no segments remain, it sets each element in the target to the provided value. * - * @param mixed $target The array or object to traverse. - * @param array $segments The segments of the dot-notation key. - * @param mixed $default The default value to return if the key is not found. - * @return mixed The retrieved value. + * @param mixed &$target The target to set values in, typically an array. + * @param array $segments The remaining segments of the dot-notation key. + * @param mixed $value The value to set. + * @param bool $overwrite If true, overwrite existing values. */ - private static function traverseGet(mixed $target, array $segments, mixed $default): mixed + private static function handleWildcardSet(mixed &$target, array $segments, mixed $value, bool $overwrite): void { - foreach ($segments as $i => $segment) { - unset($segments[$i]); - - if ($segment === null) { - return $target; - } - - if ($segment === '*') { - return static::traverseWildcard($target, $segments, $default); + if (! BaseArrayHelper::accessible($target)) { + $target = []; + } + if (! empty($segments)) { + foreach ($target as &$inner) { + static::setValue($inner, implode('.', $segments), $value, $overwrite); } - - $normalized = static::normalizeSegment($segment, $target); - $target = static::accessSegment($target, $normalized, $default); - if ($target === static::value($default)) { - return static::value($default); + } elseif ($overwrite) { + foreach ($target as &$inner) { + $inner = $value; } } - - return $target; } /** @@ -575,60 +575,74 @@ private static function normalizeSegment(string $segment, mixed $target): mixed } /** - * Access a segment in a target array or object. - * - * This method takes a target array or object, a segment, and a default value. - * It returns the value of the segment in the target if it exists, or the default - * value if it does not. It supports both array and object access. Array access - * is attempted first, then object access. + * Resolve the {first} segment for an array-like target. + * + * @param mixed $target An array or collection-like object. + * @return string|int|null The first key in the array or collection, or '{first}' if not resolved. + */ + private static function resolveFirst(mixed $target): string|int|null + { + if (( + is_object($target) || + (is_string($target) && class_exists($target)) + ) && method_exists($target, 'all')) { + $arr = $target->all(); + + return array_key_first($arr); + } elseif (is_array($target)) { + return array_key_first($target); + } + + return '{first}'; + } + + /** + * Resolves the {last} segment for an array-like target. * - * @param mixed $target The target array or object to access. - * @param mixed $segment The segment to access. - * @param mixed $default The default value to return if the segment does not exist. - * @return mixed The value of the segment in the target, or the default value. + * @param mixed $target An array or collection-like object. + * @return string|int|null The last key in the array or collection, or '{last}' if not resolved. */ - private static function accessSegment(mixed $target, mixed $segment, mixed $default): mixed + private static function resolveLast(mixed $target): string|int|null { - return match (true) { - BaseArrayHelper::accessible($target) && ArraySingle::exists($target, $segment) => $target[$segment], - is_object($target) && isset($target->{$segment}) => $target->{$segment}, - default => static::value($default), - }; + if (( + is_object($target) || + (is_string($target) && class_exists($target)) + ) && method_exists($target, 'all')) { + $arr = $target->all(); + + return array_key_last($arr); + } elseif (is_array($target)) { + return array_key_last($target); + } + + return '{last}'; } /** - * Traverse a target array/object using dot-notation with wildcard support. + * Retrieve a value from an array using an exact key path. * - * This method handles cases where a wildcard ('*') is present in the dot-notation key. - * It iterates over each element of the target, applying the remaining segments to retrieve - * the specified value. If segments contain another wildcard, the results are collapsed into - * a single array. If the target is not accessible, the default value is returned. + * If the key path is not found, the default value is returned. * - * @param mixed $target The array or object to traverse. - * @param array $segments The segments of the dot-notation key, including potential wildcards. - * @param mixed $default The default value to return if the key is not found. - * @return mixed The retrieved value(s) from the target based on the dot-notation key. + * @param mixed $array The array to retrieve the value from. + * @param string $path The key path to use for retrieval. + * @param mixed $default The default value to return if the key path is not found. + * @return mixed The retrieved value or default value. */ - private static function traverseWildcard(mixed $target, array $segments, mixed $default): mixed + private static function segmentExact(mixed $array, string $path, mixed $default): mixed { - $target = ( - is_object($target) || - (is_string($target) && class_exists($target)) - ) && method_exists($target, 'all') ? $target->all() : $target; - - if (! BaseArrayHelper::accessible($target)) { - return static::value($default); - } - - $result = []; - foreach ($target as $item) { - $result[] = static::traverseGet($item, $segments, $default); + if (! str_contains($path, '.')) { + return ArraySingle::exists($array, $path) ? $array[$path] : $default; } - if (in_array('*', $segments, true)) { - $result = ArrayMulti::collapse($result); + $parts = explode('.', $path); + foreach ($parts as $part) { + if (is_array($array) && ArraySingle::exists($array, $part)) { + $array = $array[$part]; + } else { + return $default; + } } - return $result; + return $array; } /** @@ -666,36 +680,6 @@ private static function setValue(mixed &$target, string $key, mixed $value, bool } } - /** - * Sets values in the target using dot-notation with wildcard support. - * - * This method handles cases where the first segment in the dot-notation key - * is a wildcard ('*'). It iterates over each element of the target, applying - * the remaining segments to set the specified value. If segments are present, - * it continues setting values recursively. If the overwrite flag is true and - * no segments remain, it sets each element in the target to the provided value. - * - * @param mixed &$target The target to set values in, typically an array. - * @param array $segments The remaining segments of the dot-notation key. - * @param mixed $value The value to set. - * @param bool $overwrite If true, overwrite existing values. - */ - private static function handleWildcardSet(mixed &$target, array $segments, mixed $value, bool $overwrite): void - { - if (! BaseArrayHelper::accessible($target)) { - $target = []; - } - if (! empty($segments)) { - foreach ($target as &$inner) { - static::setValue($inner, implode('.', $segments), $value, $overwrite); - } - } elseif ($overwrite) { - foreach ($target as &$inner) { - $inner = $value; - } - } - } - /** * Sets a value in the target array using dot-notation segments. * @@ -725,6 +709,28 @@ private static function setValueArray(array &$target, string $segment, array $se } } + /** + * Sets a value in a target that is not an array or object. + * + * This function is called when the target is not an array or object. + * It creates an array and sets the value in the array. + * + * @param mixed &$target The target to set the value in. + * @param string $segment The segment of the dot-notation key. + * @param array $segments The segments of the dot-notation key. + * @param mixed $value The value to set. + * @param bool $overwrite If true, overwrite any existing value. + */ + private static function setValueFallback(mixed &$target, string $segment, array $segments, mixed $value, bool $overwrite): void + { + $target = []; + if (! empty($segments)) { + static::setValue($target[$segment], implode('.', $segments), $value, $overwrite); + } elseif ($overwrite) { + $target[$segment] = $value; + } + } + /** * Sets a value in an object using dot-notation segments. * @@ -756,96 +762,89 @@ private static function setValueObject(object &$target, string $segment, array $ } /** - * Sets a value in a target that is not an array or object. + * Traverses the target array/object to retrieve a value using dot notation. * - * This function is called when the target is not an array or object. - * It creates an array and sets the value in the array. + * This method is called recursively by the `get` method to traverse the given + * array or object using dot notation. It expects the target array or object, + * the segments of the dot-notation key, and the default value to return if + * the key is not found. * - * @param mixed &$target The target to set the value in. - * @param string $segment The segment of the dot-notation key. + * @param mixed $target The array or object to traverse. * @param array $segments The segments of the dot-notation key. - * @param mixed $value The value to set. - * @param bool $overwrite If true, overwrite any existing value. + * @param mixed $default The default value to return if the key is not found. + * @return mixed The retrieved value. */ - private static function setValueFallback(mixed &$target, string $segment, array $segments, mixed $value, bool $overwrite): void + private static function traverseGet(mixed $target, array $segments, mixed $default): mixed { - $target = []; - if (! empty($segments)) { - static::setValue($target[$segment], implode('.', $segments), $value, $overwrite); - } elseif ($overwrite) { - $target[$segment] = $value; - } - } + foreach ($segments as $i => $segment) { + unset($segments[$i]); - /** - * Retrieve a value from an array using an exact key path. - * - * If the key path is not found, the default value is returned. - * - * @param mixed $array The array to retrieve the value from. - * @param string $path The key path to use for retrieval. - * @param mixed $default The default value to return if the key path is not found. - * @return mixed The retrieved value or default value. - */ - private static function segmentExact(mixed $array, string $path, mixed $default): mixed - { - if (! str_contains($path, '.')) { - return ArraySingle::exists($array, $path) ? $array[$path] : $default; - } - $parts = explode('.', $path); - foreach ($parts as $part) { - if (is_array($array) && ArraySingle::exists($array, $part)) { - $array = $array[$part]; - } else { - return $default; + if ($segment === null) { + return $target; + } + + if ($segment === '*') { + return static::traverseWildcard($target, $segments, $default); + } + + $normalized = static::normalizeSegment($segment, $target); + $target = static::accessSegment($target, $normalized, $default); + if ($target === static::value($default)) { + return static::value($default); } } - return $array; + return $target; } /** - * Resolve the {first} segment for an array-like target. + * Traverse a target array/object using dot-notation with wildcard support. * - * @param mixed $target An array or collection-like object. - * @return string|int|null The first key in the array or collection, or '{first}' if not resolved. + * This method handles cases where a wildcard ('*') is present in the dot-notation key. + * It iterates over each element of the target, applying the remaining segments to retrieve + * the specified value. If segments contain another wildcard, the results are collapsed into + * a single array. If the target is not accessible, the default value is returned. + * + * @param mixed $target The array or object to traverse. + * @param array $segments The segments of the dot-notation key, including potential wildcards. + * @param mixed $default The default value to return if the key is not found. + * @return mixed The retrieved value(s) from the target based on the dot-notation key. */ - private static function resolveFirst(mixed $target): string|int|null + private static function traverseWildcard(mixed $target, array $segments, mixed $default): mixed { - if (( + $target = ( is_object($target) || - (is_string($target) && class_exists($target)) - ) && method_exists($target, 'all')) { - $arr = $target->all(); + (is_string($target) && class_exists($target)) + ) && method_exists($target, 'all') ? $target->all() : $target; - return array_key_first($arr); - } elseif (is_array($target)) { - return array_key_first($target); + if (! BaseArrayHelper::accessible($target)) { + return static::value($default); } - return '{first}'; + $result = []; + foreach ($target as $item) { + $result[] = static::traverseGet($item, $segments, $default); + } + if (in_array('*', $segments, true)) { + $result = ArrayMulti::collapse($result); + } + + return $result; } /** - * Resolves the {last} segment for an array-like target. + * Unset a property from an object. * - * @param mixed $target An array or collection-like object. - * @return string|int|null The last key in the array or collection, or '{last}' if not resolved. + * This method removes a specified property from an object by using + * PHP's unset function. The property is directly removed from the + * object if it exists. + * + * @param object $object The object from which the property should be removed. + * @param string $property The name of the property to unset. */ - private static function resolveLast(mixed $target): string|int|null + private static function unsetProperty(object &$object, string $property): void { - if (( - is_object($target) || - (is_string($target) && class_exists($target)) - ) && method_exists($target, 'all')) { - $arr = $target->all(); - - return array_key_last($arr); - } elseif (is_array($target)) { - return array_key_last($target); - } - - return '{last}'; + unset($object->{$property}); } /** diff --git a/src/Collection/BaseCollectionTrait.php b/src/Collection/BaseCollectionTrait.php index 1316700..8b0dc7f 100644 --- a/src/Collection/BaseCollectionTrait.php +++ b/src/Collection/BaseCollectionTrait.php @@ -29,38 +29,6 @@ public function __construct(array $data = []) $this->data = $data; } - /** - * Create a new instance of the collection from the given data. - * - * This method acts as a wrapper around the `make` method, - * allowing for the creation of a collection instance using - * the provided data. The data is processed to ensure it is - * in an array format suitable for the collection. - * - * @param mixed $data The data to initialize the collection with. - * @return static A new instance of the collection. - */ - public static function from(mixed $data): static - { - return static::make($data); - } - - /** - * Create a new instance of the collection with the given data. - * - * If the given data is not an array, it will be converted to an array. - * - * @param mixed $data The data to initialize the collection with. - * @return static - */ - public static function make(mixed $data): static - { - $instance = new static([]); - $instance->data = $instance->getArrayableItems($data); - - return $instance; - } - /** * Dynamically handle calls to methods on the collection. * @@ -84,6 +52,31 @@ public function __call(string $method, array $arguments): mixed throw new BadMethodCallException("Method $method does not exist in ".static::class); } + /** + * Provide custom debug information. + */ + public function __debugInfo(): array + { + return [ + 'data' => $this->data, + 'count' => $this->count(), + ]; + } + + /** + * Magic getter to retrieve an item via property access: $collection->key + * + * This method allows for accessing collection items using property syntax. + * It internally calls the offsetGet method to retrieve the value. + * + * @param string $key The key of the item to retrieve. + * @return mixed The value associated with the given key. + */ + public function __get(string $key): mixed + { + return $this->offsetGet($key); + } + /** * Invokes the collection and returns the underlying array data. @@ -98,139 +91,156 @@ public function __invoke(): array return $this->data; } + /** - * Create and return a new Pipeline instance using the current collection's data. + * Magic isset to check for existence of an item via property access: isset($collection->key) * - * This method initializes a processing pipeline, allowing method chaining - * for array transformations or operations. + * This method allows for checking if an item exists using property syntax. + * It internally calls the offsetExists method to check for existence. * - * @return Pipeline A new pipeline instance for further processing. + * @param string $key The key of the item to check for existence. + * @return bool True if the item exists, false otherwise. */ - public function process(): Pipeline + public function __isset(string $key): bool { - return $this->pipeline ??= new Pipeline($this->data, $this); + return $this->offsetExists($key); } /** - * Retrieve an item from the collection by key or keys. + * Magic setter to set an item via property access: $collection->key = $value * - * The following cases are handled: - * - If no key is provided, the entire collection is returned. - * - If an array of keys is provided, all values are returned in an array. - * - If a single key is provided, the value is returned directly. + * This method allows for setting collection items using property syntax. + * It internally calls the offsetSet method to set the value. * - * @param string|array $keys The key(s) to retrieve. - * @return mixed The retrieved value(s). + * @param string $key The key of the item to set. + * @param mixed $value The value to set. + * @return void */ - public function get(string|array $keys): mixed + public function __set(string $key, mixed $value): void { - return DotNotation::get($this->data, $keys); + $this->offsetSet($key, $value); } - /** - * Determine if the given key or keys exist in the collection. - * - * @param string|array $keys The key(s) to check for existence. - * @return bool True if all the given keys exist in the collection, false otherwise. + * Convert the collection to a JSON string when treated as a string. */ - public function has(string|array $keys): bool + public function __toString(): string { - return DotNotation::has($this->data, $keys); + return $this->toJson(); } /** - * Check if at least one of the given keys exists in the collection. + * Magic unset to remove an item via property access: unset($collection->key) * - * This method determines whether any of the specified keys are present - * within the collection's data. It supports checking a single key - * or an array of keys. + * This method allows for removing collection items using property syntax. + * It internally calls the offsetUnset method to remove the value. * - * @param string|array $keys The key(s) to check for existence. - * @return bool True if at least one key exists, false otherwise. + * @param string $key The key of the item to remove. + * @return void */ - public function hasAny(string|array $keys): bool + public function __unset(string $key): void { - return DotNotation::hasAny($this->data, $keys); + $this->offsetUnset($key); } - /** - * Set one or multiple items in the collection using dot notation. + * Create a new instance of the collection from the given data. * - * If no key is provided, the entire collection is replaced with $value. - * If an array of key-value pairs is provided, each value is set. - * If a single key is provided, the value is set directly. + * This method acts as a wrapper around the `make` method, + * allowing for the creation of a collection instance using + * the provided data. The data is processed to ensure it is + * in an array format suitable for the collection. * - * @param array|string|null $keys The key(s) to set. - * @param mixed $value The value to set. - * @return bool True on success. + * @param mixed $data The data to initialize the collection with. + * @return static A new instance of the collection. */ - public function set(array|string|null $keys = null, mixed $value = null): bool + public static function from(mixed $data): static { - return DotNotation::set($this->data, $keys, $value); + return static::make($data); } /** - * Magic getter to retrieve an item via property access: $collection->key + * Create a new instance of the collection with the given data. * - * This method allows for accessing collection items using property syntax. - * It internally calls the offsetGet method to retrieve the value. + * If the given data is not an array, it will be converted to an array. * - * @param string $key The key of the item to retrieve. - * @return mixed The value associated with the given key. + * @param mixed $data The data to initialize the collection with. + * @return static */ - public function __get(string $key): mixed + public static function make(mixed $data): static { - return $this->offsetGet($key); + $instance = new static([]); + $instance->data = $instance->getArrayableItems($data); + + return $instance; } /** - * Magic setter to set an item via property access: $collection->key = $value + * Returns the entire array of items in this collection. * - * This method allows for setting collection items using property syntax. - * It internally calls the offsetSet method to set the value. + * This is an alias for the `items()` method. * - * @param string $key The key of the item to set. - * @param mixed $value The value to set. - * @return void + * @return array The entire array of items in this collection. */ - public function __set(string $key, mixed $value): void + public function all(): array { - $this->offsetSet($key, $value); + return $this->data; } + /** + * Clear all items from the collection. + */ + public function clear(): void + { + $this->data = []; + } + + /* + |-------------------------------------------------------------------------- + | Countable Interface + |-------------------------------------------------------------------------- + */ /** - * Magic isset to check for existence of an item via property access: isset($collection->key) + * Returns the number of items in the collection. * - * This method allows for checking if an item exists using property syntax. - * It internally calls the offsetExists method to check for existence. + * @return int The number of items in the collection. + */ + public function count(): int + { + return count($this->data); + } + + /** + * Returns the current element in the collection. * - * @param string $key The key of the item to check for existence. - * @return bool True if the item exists, false otherwise. + * This is part of the Iterator interface. + * + * @return mixed The current element in the collection. */ - public function __isset(string $key): bool + public function current(): mixed { - return $this->offsetExists($key); + return current($this->data); } /** - * Magic unset to remove an item via property access: unset($collection->key) + * Retrieve an item from the collection by key or keys. * - * This method allows for removing collection items using property syntax. - * It internally calls the offsetUnset method to remove the value. + * The following cases are handled: + * - If no key is provided, the entire collection is returned. + * - If an array of keys is provided, all values are returned in an array. + * - If a single key is provided, the value is returned directly. * - * @param string $key The key of the item to remove. - * @return void + * @param string|array $keys The key(s) to retrieve. + * @return mixed The retrieved value(s). */ - public function __unset(string $key): void + public function get(string|array $keys): mixed { - $this->offsetUnset($key); + return DotNotation::get($this->data, $keys); } @@ -256,34 +266,52 @@ public function getArrayableItems(mixed $items): array } + /* + |-------------------------------------------------------------------------- + | Iterator Interface + |-------------------------------------------------------------------------- + */ + /** - * Returns the entire array of items in this collection. + * Retrieve an external iterator. * - * This is an alias for the `items()` method. + * This method returns an instance of ArrayIterator that can be used + * to iterate over the collection's data. It is part of the IteratorAggregate + * interface, allowing for external iteration of the collection. * - * @return array The entire array of items in this collection. + * @return Traversable An iterator for the collection's data. */ - public function all(): array + public function getIterator(): Traversable { - return $this->data; + return new ArrayIterator($this->data); } + /** - * Return the raw array of items in this collection. + * Determine if the given key or keys exist in the collection. + * + * @param string|array $keys The key(s) to check for existence. + * @return bool True if all the given keys exist in the collection, false otherwise. */ - public function items(): array + public function has(string|array $keys): bool { - return $this->data; + return DotNotation::has($this->data, $keys); } + /** - * Get the collection of items as a JSON string. + * Check if at least one of the given keys exists in the collection. * - * @param int $options JSON encoding options + * This method determines whether any of the specified keys are present + * within the collection's data. It supports checking a single key + * or an array of keys. + * + * @param string|array $keys The key(s) to check for existence. + * @return bool True if at least one key exists, false otherwise. */ - public function toJson(int $options = 0): string + public function hasAny(string|array $keys): bool { - return json_encode($this->data, $options); + return DotNotation::hasAny($this->data, $keys); } /** @@ -295,19 +323,47 @@ public function isEmpty(): bool } /** - * Convert the collection to a JSON string when treated as a string. + * Return the raw array of items in this collection. */ - public function __toString(): string + public function items(): array { - return $this->toJson(); + return $this->data; } + /* + |-------------------------------------------------------------------------- + | JsonSerializable Interface + |-------------------------------------------------------------------------- + */ + /** - * Get the collection of items as a plain array. + * Convert the collection of items to an array suitable for JSON serialization. + * + * This method ensures that each item within the collection is converted + * to an array representation if it implements the JsonSerializable interface. + * Non-serializable items are returned as-is. + * + * @return array The array representation of the collection, ready for JSON serialization. */ - public function toArray(): array + public function jsonSerialize(): array { - return $this->data; + return array_map( + static fn ($value) => $value instanceof JsonSerializable ? $value->jsonSerialize() : $value, + $this->data, + ); + } + + /** + * Return the key of the current element. + * + * This is part of the Iterator interface. + * + * @return string|int|null The key of the current element, or null if the + * internal pointer is not valid. + */ + public function key(): string|int|null + { + return key($this->data); } /** @@ -319,22 +375,28 @@ public function keys(): array } /** - * Provide custom debug information. + * Merge additional items into the collection. + * + * Numeric keys are appended; string keys are overwritten by incoming items. + * + * @param mixed $items + * @return static */ - public function __debugInfo(): array + public function merge(mixed $items): static { - return [ - 'data' => $this->data, - 'count' => $this->count(), - ]; + $this->data = array_merge($this->data, $this->getArrayableItems($items)); + + return $this; } /** - * Clear all items from the collection. + * Advances the internal pointer to the next element. + * + * This is part of the Iterator interface. */ - public function clear(): void + public function next(): void { - $this->data = []; + next($this->data); } /* @@ -358,7 +420,10 @@ public function offsetExists(mixed $offset): bool if (is_string($offset) && str_contains($offset, '.')) { return DotNotation::offsetExists($this->data, $offset); } - return isset($this->data[$offset]); + if (!is_int($offset) && !is_string($offset)) { + return false; + } + return array_key_exists($offset, $this->data); } /** @@ -422,120 +487,75 @@ public function offsetUnset(mixed $offset): void unset($this->data[$offset]); } - - /* - |-------------------------------------------------------------------------- - | Iterator Interface - |-------------------------------------------------------------------------- - */ - - /** - * Retrieve an external iterator. - * - * This method returns an instance of ArrayIterator that can be used - * to iterate over the collection's data. It is part of the IteratorAggregate - * interface, allowing for external iteration of the collection. - * - * @return Traversable An iterator for the collection's data. - */ - public function getIterator(): Traversable - { - return new ArrayIterator($this->data); - } - /** - * Returns the current element in the collection. + * Create and return a new Pipeline instance using the current collection's data. * - * This is part of the Iterator interface. + * This method initializes a processing pipeline, allowing method chaining + * for array transformations or operations. * - * @return mixed The current element in the collection. + * @return Pipeline A new pipeline instance for further processing. */ - public function current(): mixed + public function process(): Pipeline { - return current($this->data); + return $this->pipeline ??= new Pipeline($this->data, $this); } /** - * Return the key of the current element. + * Rewinds the internal pointer of the collection to the first element. * * This is part of the Iterator interface. - * - * @return string|int|null The key of the current element, or null if the - * internal pointer is not valid. */ - public function key(): string|int|null + public function rewind(): void { - return key($this->data); + reset($this->data); } - /** - * Advances the internal pointer to the next element. - * - * This is part of the Iterator interface. - */ - public function next(): void - { - next($this->data); - } /** - * Checks if the current element is valid. + * Set one or multiple items in the collection using dot notation. * - * This is part of the Iterator interface. + * If no key is provided, the entire collection is replaced with $value. + * If an array of key-value pairs is provided, each value is set. + * If a single key is provided, the value is set directly. * - * @return bool True if the current element is valid, false otherwise. + * @param array|string|null $keys The key(s) to set. + * @param mixed $value The value to set. + * @return bool True on success. */ - public function valid(): bool + public function set(array|string|null $keys = null, mixed $value = null): bool { - return key($this->data) !== null; + return DotNotation::set($this->data, $keys, $value); } /** - * Rewinds the internal pointer of the collection to the first element. - * - * This is part of the Iterator interface. + * Get the collection of items as a plain array. */ - public function rewind(): void + public function toArray(): array { - reset($this->data); + return $this->data; } - /* - |-------------------------------------------------------------------------- - | Countable Interface - |-------------------------------------------------------------------------- - */ - /** - * Returns the number of items in the collection. + * Get the collection of items as a JSON string. * - * @return int The number of items in the collection. + * @param int $options JSON encoding options */ - public function count(): int + public function toJson(int $options = 0): string { - return count($this->data); - } + $json = json_encode($this->data, $options); - /* - |-------------------------------------------------------------------------- - | JsonSerializable Interface - |-------------------------------------------------------------------------- - */ + return $json === false ? 'null' : $json; + } /** - * Convert the collection of items to an array suitable for JSON serialization. + * Checks if the current element is valid. * - * This method ensures that each item within the collection is converted - * to an array representation if it implements the JsonSerializable interface. - * Non-serializable items are returned as-is. + * This is part of the Iterator interface. * - * @return array The array representation of the collection, ready for JSON serialization. + * @return bool True if the current element is valid, false otherwise. */ - public function jsonSerialize(): array + public function valid(): bool { - return array_map( - static fn ($value) => $value instanceof JsonSerializable ? $value->jsonSerialize() : $value, - $this->data, - ); + return key($this->data) !== null; } } diff --git a/src/Collection/HookedCollection.php b/src/Collection/HookedCollection.php index c9a1833..0324ec4 100644 --- a/src/Collection/HookedCollection.php +++ b/src/Collection/HookedCollection.php @@ -4,21 +4,16 @@ namespace Infocyph\ArrayKit\Collection; -use ArrayAccess; -use Countable; -use Iterator; -use JsonSerializable; +use Infocyph\ArrayKit\Array\DotNotation; use Infocyph\ArrayKit\traits\HookTrait; /** * Class HookedCollection * - * An array-based collection (implements ArrayAccess, Iterator, Countable, - * and JsonSerializable) that supports get/set hooks for dynamic transformations. + * A collection that supports get/set hooks for dynamic transformations. */ -class HookedCollection implements ArrayAccess, Iterator, Countable, JsonSerializable +class HookedCollection extends Collection { - use BaseCollectionTrait; use HookTrait; /** @@ -29,13 +24,18 @@ class HookedCollection implements ArrayAccess, Iterator, Countable, JsonSerializ * @param mixed $offset The array key * @return mixed The transformed value or null if not found */ + #[\Override] public function offsetGet(mixed $offset): mixed { if (!$this->offsetExists($offset)) { return null; } - return $this->processValue($offset, $this->data[$offset], 'get'); + $value = is_string($offset) && str_contains($offset, '.') + ? DotNotation::get($this->data, $offset) + : parent::offsetGet($offset); + + return $this->processValue($offset, $value, 'get'); } /** @@ -46,13 +46,16 @@ public function offsetGet(mixed $offset): mixed * @param mixed $offset The array key * @param mixed $value The value to set */ + #[\Override] public function offsetSet(mixed $offset, mixed $value): void { - // If offset is null, append to the array. - if ($offset === null) { - $this->data[] = $this->processValue($offset, $value, 'set'); - } else { - $this->data[$offset] = $this->processValue($offset, $value, 'set'); + $processed = $this->processValue($offset, $value, 'set'); + + if (is_string($offset) && str_contains($offset, '.')) { + DotNotation::set($this->data, $offset, $processed); + return; } + + parent::offsetSet($offset, $processed); } } diff --git a/src/Collection/Pipeline.php b/src/Collection/Pipeline.php index 5f45b41..faad170 100644 --- a/src/Collection/Pipeline.php +++ b/src/Collection/Pipeline.php @@ -4,8 +4,8 @@ namespace Infocyph\ArrayKit\Collection; -use Infocyph\ArrayKit\Array\ArraySingle; use Infocyph\ArrayKit\Array\ArrayMulti; +use Infocyph\ArrayKit\Array\ArraySingle; use Infocyph\ArrayKit\Array\BaseArrayHelper; class Pipeline @@ -19,60 +19,39 @@ public function __construct( ) { } - /* - |-------------------------------------------------------------------------- - | ArraySingle-based chainable methods (Single-Dimensional usage) - |-------------------------------------------------------------------------- - */ - /** - * Keep only certain keys in the array, using ArraySingle::only. - * (Typically relevant if the array is associative 1D.) - */ - public function only(array|string $keys): Collection - { - $this->working = ArraySingle::only($this->working, $keys); - return $this->collection; - } - - /** - * Return every n-th element, using ArraySingle::nth. + * Quick example: Check if at least one item passes a truth test, from ArraySingle::some or ArrayMulti::some + * Not chainable, returns bool. */ - public function nth(int $step, int $offset = 0): Collection + public function any(callable $callback): bool { - $this->working = ArraySingle::nth($this->working, $step, $offset); - return $this->collection; + return ArraySingle::some($this->working, $callback); } /** - * Keep only duplicate values, using ArraySingle::duplicates. - * (Typically this means setting $this->working to the *list of duplicates*.) + * Filter rows where a certain key is between two values, using ArrayMulti::between. */ - public function duplicates(): Collection + public function between(string $key, float|int $from, float|int $to): Collection { - // If you want to *replace* the original array with only duplicates: - $dupes = ArraySingle::duplicates($this->working); - // This means our collection now becomes an array of those duplicated values. - // Possibly you might want to keep them in a "counts" structure, but let's do direct. - $this->working = $dupes; + $this->working = ArrayMulti::between($this->working, $key, $from, $to); return $this->collection; } /** - * Slice the array (like array_slice) using ArraySingle::slice. + * chunk the array (single-dim). */ - public function slice(int $offset, ?int $length = null): Collection + public function chunk(int $size, bool $preserveKeys = false): Collection { - $this->working = ArraySingle::slice($this->working, $offset, $length); + $this->working = ArraySingle::chunk($this->working, $size, $preserveKeys); return $this->collection; } /** - * "Paginate" the array by slicing it into a smaller segment, using ArraySingle::paginate. + * Collapses an array of arrays into a single (1D) array (2D -> 1D). */ - public function paginate(int $page, int $perPage): Collection + public function collapse(): Collection { - $this->working = ArraySingle::paginate($this->working, $page, $perPage); + $this->working = ArrayMulti::collapse($this->working); return $this->collection; } @@ -89,11 +68,23 @@ public function combine(array $values): Collection } /** - * Map each element, updating $this->working. (From prior example) + * Keep only duplicate values, using ArraySingle::duplicates. + * (Typically this means setting $this->working to the *list of duplicates*.) */ - public function map(callable $callback): Collection + public function duplicates(): Collection { - $this->working = ArraySingle::map($this->working, $callback); + // If you want to *replace* the original array with only duplicates: + $dupes = ArraySingle::duplicates($this->working); + // This means our collection now becomes an array of those duplicated values. + // Possibly you might want to keep them in a "counts" structure, but let's do direct. + $this->working = $dupes; + return $this->collection; + } + + /** Remove keys (inverse of only) */ + public function except(array|string $keys): Collection + { + $this->working = ArraySingle::except($this->working, $keys); return $this->collection; } @@ -108,232 +99,229 @@ public function filter(callable $callback): Collection } /** - * chunk the array (single-dim). + * Return the first item in a 2D array, or single-dim array, depending on usage. + * from ArrayMulti::first or direct approach. */ - public function chunk(int $size, bool $preserveKeys = false): Collection + public function first(?callable $callback = null, mixed $default = null): mixed { - $this->working = ArraySingle::chunk($this->working, $size, $preserveKeys); - return $this->collection; + return ArrayMulti::first($this->working, $callback, $default); } + /* + |-------------------------------------------------------------------------- + | ArrayMulti-based chainable methods (Multi-Dimensional usage) + |-------------------------------------------------------------------------- + */ + /** - * Return only unique values using ArraySingle::unique. + * Flatten the array using ArrayMulti::flatten. */ - public function unique(bool $strict = false): Collection + public function flatten(float|int $depth = \INF): Collection { - $this->working = ArraySingle::unique($this->working, $strict); + $this->working = ArrayMulti::flatten($this->working, $depth); return $this->collection; } /** - * Reject certain items (inverse of filter), using ArraySingle::reject + * Flatten the array into a single level but preserve keys, using flattenByKey. */ - public function reject(mixed $callback = true): Collection + public function flattenByKey(): Collection { - $this->working = ArraySingle::reject($this->working, $callback); + $this->working = ArrayMulti::flattenByKey($this->working); return $this->collection; } /** - * Skip the first N items. + * Group a 2D array by a given column or callback, using ArrayMulti::groupBy. */ - public function skip(int $count): Collection + public function groupBy(string|callable $groupBy, bool $preserveKeys = false): Collection { - $this->working = ArraySingle::skip($this->working, $count); + $this->working = ArrayMulti::groupBy($this->working, $groupBy, $preserveKeys); return $this->collection; } + /* + |-------------------------------------------------------------------------- + | BaseArrayHelper-based chainable or one-time checks + |-------------------------------------------------------------------------- + */ + /** - * Skip items while callback returns true; once false, keep remainder. + * Check if the current array is multiDimensional, from BaseArrayHelper (not chainable). */ - public function skipWhile(callable $callback): Collection + public function isMultiDimensional(): bool { - $this->working = ArraySingle::skipWhile($this->working, $callback); - return $this->collection; + return BaseArrayHelper::isMultiDimensional($this->working); } /** - * Skip items until callback returns true, then keep remainder. + * Return the last item in a 2D array, or single-dim array, depending on usage. */ - public function skipUntil(callable $callback): Collection + public function last(?callable $callback = null, mixed $default = null): mixed { - $this->working = ArraySingle::skipUntil($this->working, $callback); - return $this->collection; + return ArrayMulti::last($this->working, $callback, $default); } /** - * Partition [passed, failed]. + * Map each element, updating $this->working. (From prior example) */ - public function partition(callable $callback): Collection + public function map(callable $callback): Collection { - $this->working = ArraySingle::partition($this->working, $callback); + $this->working = ArraySingle::map($this->working, $callback); return $this->collection; } - /* - |-------------------------------------------------------------------------- - | ArrayMulti-based chainable methods (Multi-Dimensional usage) - |-------------------------------------------------------------------------- - */ - - /** - * Flatten the array using ArrayMulti::flatten. - */ - public function flatten(float|int $depth = \INF): Collection + /** Return the statistical median – TERMINATES chain (scalar) */ + public function median(): float|int { - $this->working = ArrayMulti::flatten($this->working, $depth); - return $this->collection; + return ArraySingle::median($this->working); } - /** - * Flatten the array into a single level but preserve keys, using flattenByKey. - */ - public function flattenByKey(): Collection + /** Return the statistical mode(s) – TERMINATES chain (array) */ + public function mode(): array { - $this->working = ArrayMulti::flattenByKey($this->working); - return $this->collection; + return ArraySingle::mode($this->working); } /** - * Recursively sort the array by keys/values, using ArrayMulti::sortRecursive. + * Return every n-th element, using ArraySingle::nth. */ - public function sortRecursive(int $options = SORT_REGULAR, bool $descending = false): Collection + public function nth(int $step, int $offset = 0): Collection { - $this->working = ArrayMulti::sortRecursive($this->working, $options, $descending); + $this->working = ArraySingle::nth($this->working, $step, $offset); return $this->collection; } + /* + |-------------------------------------------------------------------------- + | ArraySingle-based chainable methods (Single-Dimensional usage) + |-------------------------------------------------------------------------- + */ + /** - * Collapses an array of arrays into a single (1D) array (2D -> 1D). + * Keep only certain keys in the array, using ArraySingle::only. + * (Typically relevant if the array is associative 1D.) */ - public function collapse(): Collection + public function only(array|string $keys): Collection { - $this->working = ArrayMulti::collapse($this->working); + $this->working = ArraySingle::only($this->working, $keys); return $this->collection; } /** - * Group a 2D array by a given column or callback, using ArrayMulti::groupBy. + * "Paginate" the array by slicing it into a smaller segment, using ArraySingle::paginate. */ - public function groupBy(string|callable $groupBy, bool $preserveKeys = false): Collection + public function paginate(int $page, int $perPage): Collection { - $this->working = ArrayMulti::groupBy($this->working, $groupBy, $preserveKeys); + $this->working = ArraySingle::paginate($this->working, $page, $perPage); return $this->collection; } /** - * Filter rows where a certain key is between two values, using ArrayMulti::between. + * Partition [passed, failed]. */ - public function between(string $key, float|int $from, float|int $to): Collection + public function partition(callable $callback): Collection { - $this->working = ArrayMulti::between($this->working, $key, $from, $to); + $this->working = ArraySingle::partition($this->working, $callback); return $this->collection; } /** - * Filter using a custom callback on each row, using ArrayMulti::whereCallback. + * Pipe the working array through a callback, replacing it with whatever you return. + * + * @param callable $callback fn(array $working): array + * @return Collection */ - public function whereCallback(?callable $callback = null, mixed $default = null): Collection + public function pipe(callable $callback): Collection { - $this->working = ArrayMulti::whereCallback($this->working, $callback, $default); + $this->working = $callback($this->working); return $this->collection; } - /** - * Filter rows by a single key's comparison (like ->where('age', '>', 18)). - */ - public function where(string $key, mixed $operator = null, mixed $value = null): Collection + /** Extract a column (optionally re-index) */ + public function pluck(string $column, ?string $indexBy = null): Collection { - $this->working = ArrayMulti::where($this->working, $key, $operator, $value); + $this->working = ArrayMulti::pluck($this->working, $column, $indexBy); return $this->collection; } /** - * Filter rows where "column" matches one of the given values. + * Return a "reduced" single value from the array (like sum-of-squares), from ArraySingle::reduce. */ - public function whereIn(string $key, array $values, bool $strict = false): Collection + public function reduce(callable $callback, mixed $initial = null): mixed { - $this->working = ArrayMulti::whereIn($this->working, $key, $values, $strict); - return $this->collection; + return ArraySingle::reduce($this->working, $callback, $initial); } /** - * Filter rows where "column" is not in the given values. + * Reject certain items (inverse of filter), using ArraySingle::reject */ - public function whereNotIn(string $key, array $values, bool $strict = false): Collection + public function reject(mixed $callback = true): Collection { - $this->working = ArrayMulti::whereNotIn($this->working, $key, $values, $strict); + $this->working = ArraySingle::reject($this->working, $callback); return $this->collection; } /** - * Filter rows where a column is null, using ArrayMulti::whereNull. + * Shuffle the array in place, from ArraySingle::shuffle or BaseArrayHelper logic. */ - public function whereNull(string $key): Collection + public function shuffle(?int $seed = null): Collection { - $this->working = ArrayMulti::whereNull($this->working, $key); + $this->working = ArraySingle::shuffle($this->working, $seed); return $this->collection; } /** - * Filter rows where a column is NOT null, using ArrayMulti::whereNotNull. + * Skip the first N items. */ - public function whereNotNull(string $key): Collection + public function skip(int $count): Collection { - $this->working = ArrayMulti::whereNotNull($this->working, $key); + $this->working = ArraySingle::skip($this->working, $count); return $this->collection; } /** - * Sort by a column or callback in ascending/descending order. + * Skip items until callback returns true, then keep remainder. */ - public function sortBy(string|callable $by, bool $desc = false, int $options = SORT_REGULAR): Collection + public function skipUntil(callable $callback): Collection { - $this->working = ArrayMulti::sortBy($this->working, $by, $desc, $options); + $this->working = ArraySingle::skipUntil($this->working, $callback); return $this->collection; } - /* - |-------------------------------------------------------------------------- - | BaseArrayHelper-based chainable or one-time checks - |-------------------------------------------------------------------------- - */ - /** - * Check if the current array is multiDimensional, from BaseArrayHelper (not chainable). + * Skip items while callback returns true; once false, keep remainder. */ - public function isMultiDimensional(): bool + public function skipWhile(callable $callback): Collection { - return BaseArrayHelper::isMultiDimensional($this->working); + $this->working = ArraySingle::skipWhile($this->working, $callback); + return $this->collection; } /** - * Wrap the entire array if it's not already an array, from BaseArrayHelper::wrap + * Slice the array (like array_slice) using ArraySingle::slice. */ - public function wrap(): Collection + public function slice(int $offset, ?int $length = null): Collection { - $this->working = BaseArrayHelper::wrap($this->working); + $this->working = ArraySingle::slice($this->working, $offset, $length); return $this->collection; } /** - * Example: Unwrap an array if it has exactly one element, from BaseArrayHelper::unWrap. + * Sort by a column or callback in ascending/descending order. */ - public function unWrap(): Collection + public function sortBy(string|callable $by, bool $desc = false, int $options = SORT_REGULAR): Collection { - // Might produce a non-array, so up to you if you want to store that as $working... - $unwrapped = BaseArrayHelper::unWrap($this->working); - // If $unwrapped is not array, we store it as a single-element array to keep chain consistent - $this->working = is_array($unwrapped) ? $unwrapped : [$unwrapped]; + $this->working = ArrayMulti::sortBy($this->working, $by, $desc, $options); return $this->collection; } /** - * Shuffle the array in place, from ArraySingle::shuffle or BaseArrayHelper logic. + * Recursively sort the array by keys/values, using ArrayMulti::sortRecursive. */ - public function shuffle(?int $seed = null): Collection + public function sortRecursive(int $options = SORT_REGULAR, bool $descending = false): Collection { - $this->working = ArraySingle::shuffle($this->working, $seed); + $this->working = ArrayMulti::sortRecursive($this->working, $options, $descending); return $this->collection; } @@ -353,125 +341,137 @@ public function sum(?callable $callback = null): float|int } /** - * Return the first item in a 2D array, or single-dim array, depending on usage. - * from ArrayMulti::first or direct approach. + * Tap into the current working array for side-effects (debug/log), then continue. + * + * @param callable $callback fn(array $working): void + * @return Collection */ - public function first(?callable $callback = null, mixed $default = null): mixed + public function tap(callable $callback): Collection { - return ArrayMulti::first($this->working, $callback, $default); + $callback($this->working); + return $this->collection; } - /** - * Return the last item in a 2D array, or single-dim array, depending on usage. - */ - public function last(?callable $callback = null, mixed $default = null): mixed + /** Matrix transpose (rows ↔ columns) */ + public function transpose(): Collection { - return ArrayMulti::last($this->working, $callback, $default); + $this->working = ArrayMulti::transpose($this->working); + return $this->collection; } /** - * Return a "reduced" single value from the array (like sum-of-squares), from ArraySingle::reduce. + * Return only unique values using ArraySingle::unique. */ - public function reduce(callable $callback, mixed $initial = null): mixed + public function unique(bool $strict = false): Collection { - return ArraySingle::reduce($this->working, $callback, $initial); + $this->working = ArraySingle::unique($this->working, $strict); + return $this->collection; } /** - * Quick example: Check if at least one item passes a truth test, from ArraySingle::some or ArrayMulti::some - * Not chainable, returns bool. + * Inverse of when(): only run if $condition is false. + * + * @param bool $condition + * @param callable $callback + * @param callable|null $default + * @return Collection */ - public function any(callable $callback): bool + public function unless(bool $condition, callable $callback, ?callable $default = null): Collection { - return ArraySingle::some($this->working, $callback); + return $this->when(! $condition, $callback, $default); } - /** Remove keys (inverse of only) */ - public function except(array|string $keys): Collection + /** + * Example: Unwrap an array if it has exactly one element, from BaseArrayHelper::unWrap. + */ + public function unWrap(): Collection { - $this->working = ArraySingle::except($this->working, $keys); + // Might produce a non-array, so up to you if you want to store that as $working... + $unwrapped = BaseArrayHelper::unWrap($this->working); + // If $unwrapped is not array, we store it as a single-element array to keep chain consistent + $this->working = is_array($unwrapped) ? $unwrapped : [$unwrapped]; return $this->collection; } - /** Return the statistical median – TERMINATES chain (scalar) */ - public function median(): float|int + /** + * Conditionally apply one of two callbacks based on $condition. + * + * @param bool $condition + * @param callable $callback fn(array $working): array + * @param callable|null $default fn(array $working): array + * @return Collection + */ + public function when(bool $condition, callable $callback, ?callable $default = null): Collection { - return ArraySingle::median($this->working); + if ($condition) { + $this->working = $callback($this->working); + } elseif ($default) { + $this->working = $default($this->working); + } + return $this->collection; } - /** Return the statistical mode(s) – TERMINATES chain (array) */ - public function mode(): array + /** + * Filter rows by a single key's comparison (like ->where('age', '>', 18)). + */ + public function where(string $key, mixed $operator = null, mixed $value = null): Collection { - return ArraySingle::mode($this->working); + $this->working = ArrayMulti::where($this->working, $key, $operator, $value); + return $this->collection; } - /** Extract a column (optionally re-index) */ - public function pluck(string $column, ?string $indexBy = null): Collection + /** + * Filter using a custom callback on each row, using ArrayMulti::whereCallback. + */ + public function whereCallback(?callable $callback = null, mixed $default = null): Collection { - $this->working = ArrayMulti::pluck($this->working, $column, $indexBy); + $this->working = ArrayMulti::whereCallback($this->working, $callback, $default); return $this->collection; } - /** Matrix transpose (rows ↔ columns) */ - public function transpose(): Collection + /** + * Filter rows where "column" matches one of the given values. + */ + public function whereIn(string $key, array $values, bool $strict = false): Collection { - $this->working = ArrayMulti::transpose($this->working); + $this->working = ArrayMulti::whereIn($this->working, $key, $values, $strict); return $this->collection; } /** - * Tap into the current working array for side-effects (debug/log), then continue. - * - * @param callable $callback fn(array $working): void - * @return Collection + * Filter rows where "column" is not in the given values. */ - public function tap(callable $callback): Collection + public function whereNotIn(string $key, array $values, bool $strict = false): Collection { - $callback($this->working); + $this->working = ArrayMulti::whereNotIn($this->working, $key, $values, $strict); return $this->collection; } /** - * Pipe the working array through a callback, replacing it with whatever you return. - * - * @param callable $callback fn(array $working): array - * @return Collection + * Filter rows where a column is NOT null, using ArrayMulti::whereNotNull. */ - public function pipe(callable $callback): Collection + public function whereNotNull(string $key): Collection { - $this->working = $callback($this->working); + $this->working = ArrayMulti::whereNotNull($this->working, $key); return $this->collection; } /** - * Conditionally apply one of two callbacks based on $condition. - * - * @param bool $condition - * @param callable $callback fn(array $working): array - * @param callable|null $default fn(array $working): array - * @return Collection + * Filter rows where a column is null, using ArrayMulti::whereNull. */ - public function when(bool $condition, callable $callback, ?callable $default = null): Collection + public function whereNull(string $key): Collection { - if ($condition) { - $this->working = $callback($this->working); - } elseif ($default) { - $this->working = $default($this->working); - } + $this->working = ArrayMulti::whereNull($this->working, $key); return $this->collection; } /** - * Inverse of when(): only run if $condition is false. - * - * @param bool $condition - * @param callable $callback - * @param callable|null $default - * @return Collection + * Wrap the entire array if it's not already an array, from BaseArrayHelper::wrap */ - public function unless(bool $condition, callable $callback, ?callable $default = null): Collection + public function wrap(): Collection { - return $this->when(! $condition, $callback, $default); + $this->working = BaseArrayHelper::wrap($this->working); + return $this->collection; } } diff --git a/src/Config/BaseConfigTrait.php b/src/Config/BaseConfigTrait.php index 4de17c8..d381d95 100644 --- a/src/Config/BaseConfigTrait.php +++ b/src/Config/BaseConfigTrait.php @@ -14,65 +14,52 @@ trait BaseConfigTrait protected array $items = []; /** - * Load configuration from a specified file path (PHP returning array). - * - * @param string $path The file path to load - * @return bool True if loaded successfully, false if already loaded or file missing - */ - public function loadFile(string $path): bool - { - if (count($this->items) === 0 && file_exists($path)) { - $this->items = include $path; - return true; - } - return false; - } - - /** - * Load configuration directly from an array resource. + * Retrieve all configuration items. * - * @param array $resource The array containing config items - * @return bool True if loaded successfully, false if already loaded + * @return array The entire configuration array */ - public function loadArray(array $resource): bool + public function all(): array { - if (count($this->items) === 0) { - $this->items = $resource; - return true; - } - return false; + return $this->items; } /** - * Retrieve all configuration items. + * Append a value to a configuration array at the specified key. * - * @return array The entire configuration array + * @param string $key The dot-notation key referencing an array + * @param mixed $value The value to append + * @return bool True on success */ - public function all(): array + public function append(string $key, mixed $value): bool { - return $this->items; + $array = $this->get($key, []); + $array[] = $value; + return $this->set($key, $array); } /** - * Check if one or multiple keys exist in the configuration (no wildcard). + * "Fill" config data where it's missing, i.e. DotNotation's fill logic. * - * @param string|array $keys Dot-notation key(s) - * @return bool True if the key(s) exist + * @param string|array $key Dot-notation key or multiple [key => value] + * @param mixed|null $value The value to set if missing + * @return bool */ - public function has(string|array $keys): bool + public function fill(string|array $key, mixed $value = null): bool { - return DotNotation::has($this->items, $keys); + DotNotation::fill($this->items, $key, $value); + return true; } /** - * Check if *any* of the given keys exist (no wildcard). + * Remove/unset a key (or keys) from configuration using dot notation + wildcard expansions. * - * @param string|array $keys Dot-notation key(s) - * @return bool True if at least one key exists + * @param string|int|array $key + * @return bool */ - public function hasAny(string|array $keys): bool + public function forget(string|int|array $key): bool { - return DotNotation::hasAny($this->items, $keys); + DotNotation::forget($this->items, $key); + return true; } /** @@ -83,50 +70,61 @@ public function hasAny(string|array $keys): bool * @param mixed|null $default Default value if key not found * @return mixed The value(s) found or default */ - public function get(string|int|array $key = null, mixed $default = null): mixed + public function get(string|int|array|null $key = null, mixed $default = null): mixed { return DotNotation::get($this->items, $key, $default); } /** - * Set a configuration value by dot-notation key (wildcard support), - * optionally controlling overwrite vs. fill-like behavior. + * Check if one or multiple keys exist in the configuration (no wildcard). * - * If no key is provided, replaces the entire config array with $value. + * @param string|array $keys Dot-notation key(s) + * @return bool True if the key(s) exist + */ + public function has(string|array $keys): bool + { + return DotNotation::has($this->items, $keys); + } + + /** + * Check if *any* of the given keys exist (no wildcard). * - * @param string|array|null $key Dot-notation key or [key => value] array - * @param mixed|null $value The value to set - * @param bool $overwrite Overwrite existing? Default true. - * @return bool True on success + * @param string|array $keys Dot-notation key(s) + * @return bool True if at least one key exists */ - public function set(string|array|null $key = null, mixed $value = null, bool $overwrite = true): bool + public function hasAny(string|array $keys): bool { - return DotNotation::set($this->items, $key, $value, $overwrite); + return DotNotation::hasAny($this->items, $keys); } /** - * "Fill" config data where it's missing, i.e. DotNotation's fill logic. + * Load configuration directly from an array resource. * - * @param string|array $key Dot-notation key or multiple [key => value] - * @param mixed|null $value The value to set if missing - * @return bool + * @param array $resource The array containing config items + * @return bool True if loaded successfully, false if already loaded */ - public function fill(string|array $key, mixed $value = null): bool + public function loadArray(array $resource): bool { - DotNotation::fill($this->items, $key, $value); - return true; + if (count($this->items) === 0) { + $this->items = $resource; + return true; + } + return false; } /** - * Remove/unset a key (or keys) from configuration using dot notation + wildcard expansions. + * Load configuration from a specified file path (PHP returning array). * - * @param string|int|array $key - * @return bool + * @param string $path The file path to load + * @return bool True if loaded successfully, false if already loaded or file missing */ - public function forget(string|int|array $key): bool + public function loadFile(string $path): bool { - DotNotation::forget($this->items, $key); - return true; + if (count($this->items) === 0 && file_exists($path)) { + $this->items = include $path; + return true; + } + return false; } /** @@ -145,16 +143,18 @@ public function prepend(string $key, mixed $value): bool } /** - * Append a value to a configuration array at the specified key. + * Set a configuration value by dot-notation key (wildcard support), + * optionally controlling overwrite vs. fill-like behavior. * - * @param string $key The dot-notation key referencing an array - * @param mixed $value The value to append + * If no key is provided, replaces the entire config array with $value. + * + * @param string|array|null $key Dot-notation key or [key => value] array + * @param mixed|null $value The value to set + * @param bool $overwrite Overwrite existing? Default true. * @return bool True on success */ - public function append(string $key, mixed $value): bool + public function set(string|array|null $key = null, mixed $value = null, bool $overwrite = true): bool { - $array = $this->get($key, []); - $array[] = $value; - return $this->set($key, $array); + return DotNotation::set($this->items, $key, $value, $overwrite); } } diff --git a/src/Config/DynamicConfig.php b/src/Config/DynamicConfig.php index 22d8a93..c6774b8 100644 --- a/src/Config/DynamicConfig.php +++ b/src/Config/DynamicConfig.php @@ -13,16 +13,52 @@ class DynamicConfig use HookTrait; + /** + * "Fill" config data where it's missing, i.e. DotNotation's fill logic, + * applying any "on set" hooks to the value. + * + * @param string|array $key Dot-notation key or multiple [key => value] + * @param mixed|null $value The value to set if missing + * @return bool True on success + */ + public function fill(string|array $key, mixed $value = null): bool + { + if (is_array($key)) { + $processed = []; + foreach ($key as $path => $entry) { + $processed[$path] = $this->processValue($path, $entry, 'set'); + } + + DotNotation::fill($this->items, $processed); + + return true; + } + + $processed = $this->processValue($key, $value, 'set'); + DotNotation::fill($this->items, $key, $processed); + return true; + } + + /** * Retrieves a configuration value by dot-notation key, applying any "on get" hooks. * - * @param int|string|null $key The key to retrieve (supports dot notation) + * @param int|string|array|null $key The key(s) to retrieve (supports dot notation) * @param mixed $default The default value to return if the key is not found * @return mixed The retrieved value */ - public function get(int|string $key = null, mixed $default = null): mixed + public function get(int|string|array|null $key = null, mixed $default = null): mixed { $value = DotNotation::get($this->items, $key, $default); + + if (is_array($key)) { + foreach ($value as $path => $entry) { + $value[$path] = $this->processValue($path, $entry, 'get'); + } + + return $value; + } + return $this->processValue($key, $value, 'get'); } @@ -30,31 +66,24 @@ public function get(int|string $key = null, mixed $default = null): mixed /** * Sets a configuration value by dot-notation key, applying any "on set" hooks. * - * @param string|null $key The key to set (supports dot notation) + * @param string|array|null $key The key to set (supports dot notation) * @param mixed $value The value to set * @param bool $overwrite If true, overwrite existing values; otherwise, fill in missing (default true) * @return bool True on success */ - public function set(?string $key = null, mixed $value = null, bool $overwrite = true): bool + public function set(string|array|null $key = null, mixed $value = null, bool $overwrite = true): bool { - // The user might want the dynamic config to also accept $overwrite param for fill-like usage - $processedValue = $this->processValue($key, $value, 'set'); - return DotNotation::set($this->items, $key, $processedValue, $overwrite); - } + if (is_array($key)) { + $processed = []; + foreach ($key as $path => $entry) { + $processed[$path] = $this->processValue($path, $entry, 'set'); + } + return DotNotation::set($this->items, $processed, null, $overwrite); + } - /** - * "Fill" config data where it's missing, i.e. DotNotation's fill logic, - * applying any "on set" hooks to the value. - * - * @param string|array $key Dot-notation key or multiple [key => value] - * @param mixed|null $value The value to set if missing - * @return bool True on success - */ - public function fill(string|array $key, mixed $value = null): bool - { - $processed = $this->processValue($key, $value, 'set'); - DotNotation::fill($this->items, $key, $processed); - return true; + $processedValue = $this->processValue($key, $value, 'set'); + + return DotNotation::set($this->items, $key, $processedValue, $overwrite); } } diff --git a/src/traits/DTOTrait.php b/src/traits/DTOTrait.php index d800dee..b237ae0 100644 --- a/src/traits/DTOTrait.php +++ b/src/traits/DTOTrait.php @@ -34,15 +34,26 @@ trait DTOTrait */ public static function create(array $values): static { - $instance = new static(); + return (new static())->fromArray($values); + } + /** + * Populate the current object from an array of values. + * + * Unknown keys are ignored. + * + * @param array $values Key-value pairs matching property names + * @return static + */ + public function fromArray(array $values): static + { foreach ($values as $key => $value) { - if (property_exists($instance, $key)) { - $instance->{$key} = $value; + if (property_exists($this, $key)) { + $this->{$key} = $value; } } - return $instance; + return $this; } /** diff --git a/src/traits/HookTrait.php b/src/traits/HookTrait.php index 57861a8..98816a6 100644 --- a/src/traits/HookTrait.php +++ b/src/traits/HookTrait.php @@ -70,6 +70,18 @@ protected function addHook(mixed $offset, string $direction, callable $callback) return $this; } + /** + * Construct the internal key for hooking, e.g. "offset-get" or "offset-set". + * + * @param string $hook The offset or key + * @param string $direction Either "get" or "set" + * @return string + */ + protected function getHookName(string $hook, string $direction): string + { + return $hook . '-' . $direction; + } + /** * Apply any relevant hooks to a value before returning or storing it. * @@ -89,16 +101,4 @@ protected function processValue(mixed $offset, mixed $value, string $direction): return $value; } - - /** - * Construct the internal key for hooking, e.g. "offset-get" or "offset-set". - * - * @param string $hook The offset or key - * @param string $direction Either "get" or "set" - * @return string - */ - protected function getHookName(string $hook, string $direction): string - { - return $hook . '-' . $direction; - } } diff --git a/tests/Feature/ArraySingleTest.php b/tests/Feature/ArraySingleTest.php index 6bf9f3b..deb1003 100644 --- a/tests/Feature/ArraySingleTest.php +++ b/tests/Feature/ArraySingleTest.php @@ -25,6 +25,10 @@ ->and(ArraySingle::isList($assoc))->toBeFalse(); }); +it('treats an empty array as a list', function () { + expect(ArraySingle::isList([]))->toBeTrue(); +}); + it('calculates average of numeric values', function () { $nums = [2, 4, 6, 8]; expect(ArraySingle::avg($nums))->toBe(5); diff --git a/tests/Feature/BucketCollectionTest.php b/tests/Feature/BucketCollectionTest.php index 0381802..b335355 100644 --- a/tests/Feature/BucketCollectionTest.php +++ b/tests/Feature/BucketCollectionTest.php @@ -24,16 +24,22 @@ expect($keys)->toBe(['a', 'b']); }); -//it('provides a merge method', function () { -// $c1 = new Collection(['a' => 1]); -// $c2 = new Collection(['b' => 2]); -// -// $c1->merge($c2); -// expect($c1->items())->toBe(['a' => 1, 'b' => 2]); -//}); +it('provides a merge method', function () { + $c1 = new Collection(['a' => 1]); + $c2 = new Collection(['b' => 2]); + + $c1->merge($c2); + expect($c1->items())->toBe(['a' => 1, 'b' => 2]); +}); it('can filter and return a new collection', function () { $collection = new Collection([1, 2, 3, 4]); $even = $collection->filter(fn ($val) => $val % 2 === 0); expect($even->all())->toBe([1 => 2, 3 => 4]); }); + +it('considers null-valued keys as existing', function () { + $collection = new Collection(['nullable' => null]); + + expect(isset($collection['nullable']))->toBeTrue(); +}); diff --git a/tests/Feature/DTOTraitTest.php b/tests/Feature/DTOTraitTest.php index 77bcc98..d90f36f 100644 --- a/tests/Feature/DTOTraitTest.php +++ b/tests/Feature/DTOTraitTest.php @@ -21,3 +21,18 @@ 'age' => 30, ]); }); + +it('can hydrate an existing DTO instance from an array', function () { + $dto = new class { + use DTOTrait; + public string $name = ''; + public int $age = 0; + }; + + $dto->fromArray(['name' => 'Bob', 'age' => 28, 'ignored' => true]); + + expect($dto->toArray())->toBe([ + 'name' => 'Bob', + 'age' => 28, + ]); +}); diff --git a/tests/Feature/DynamicConfigTest.php b/tests/Feature/DynamicConfigTest.php index 5e5aca5..bb748e8 100644 --- a/tests/Feature/DynamicConfigTest.php +++ b/tests/Feature/DynamicConfigTest.php @@ -28,3 +28,31 @@ // The stored value should be uppercase expect($this->dynamic->get('user.name'))->toBe('JOHN'); }); + +it('supports hooks with bulk set/get operations', function () { + $this->dynamic->onSet('user.name', fn ($value) => strtoupper($value)); + $this->dynamic->onGet('user.name', fn ($value) => strtolower($value)); + + $this->dynamic->set([ + 'user.name' => 'alice', + 'user.email' => 'alice@example.com', + ]); + + expect($this->dynamic->get(['user.name', 'user.email']))->toBe([ + 'user.name' => 'alice', + 'user.email' => 'alice@example.com', + ]); +}); + +it('supports bulk fill operations without overwriting existing keys', function () { + $this->dynamic->set('app.name', 'ArrayKit'); + + $this->dynamic->fill([ + 'app.name' => 'ShouldNotReplace', + 'app.env' => 'local', + ]); + + expect($this->dynamic->get('app.name')) + ->toBe('ArrayKit') + ->and($this->dynamic->get('app.env'))->toBe('local'); +}); diff --git a/tests/Feature/HookedCollectionTest.php b/tests/Feature/HookedCollectionTest.php index 577c3b6..063490a 100644 --- a/tests/Feature/HookedCollectionTest.php +++ b/tests/Feature/HookedCollectionTest.php @@ -29,3 +29,24 @@ $collection['test'] = 123; expect($collection['test'])->toBe(123); }); + +it('supports hooks for dot-notation keys', function () { + $collection = new HookedCollection(['user' => ['name' => 'alice']]); + $collection->onGet('user.name', fn ($value) => strtoupper((string) $value)); + $collection->onSet('user.role', fn ($value) => "Role: $value"); + + $collection['user.role'] = 'admin'; + + expect($collection['user.name']) + ->toBe('ALICE') + ->and($collection['user.role'])->toBe('Role: admin'); +}); + +it('can run pipeline operations without type errors', function () { + $collection = new HookedCollection([1, 2, 3, 4]); + $filtered = $collection->filter(fn ($value) => $value % 2 === 0); + + expect($filtered) + ->toBeInstanceOf(HookedCollection::class) + ->and($filtered->all())->toBe([1 => 2, 3 => 4]); +});