diff --git a/.gitattributes b/.gitattributes
index 5d1a411..a3648c5 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,7 +1,43 @@
-/tests export-ignore
-/README.md export-ignore
+# Autodetect text files
+* text=auto
+
+
+# ...Unless the name matches the following overriding patterns
+
+# Definitively text files
+*.php text
+*.css text
+*.js text
+*.txt text
+*.md text
+*.xml text
+*.json text
+*.bat text
+*.sql text
+*.yml text
+*.neon text
+
+
+# Ensure those won't be messed up with
+*.png binary
+*.jpg binary
+*.gif binary
+*.ttf binary
+
+
+# Ignore some meta files when creating an archive of this repository
+/.github export-ignore
+/.editorconfig export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
+/phpunit.xml export-ignore
+/phpcs.xml export-ignore
+/phpmd.xml export-ignore
+/phpstan.neon export-ignore
/.travis.yml export-ignore
-/phpunit.xml.dist export-ignore
-/phpcs.xml.dist export-ignore
+/.scrutinizer.yml export-ignore
+
+
+# Avoid merge conflicts in CHANGELOG
+# https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/
+/CHANGELOG.md merge=union
diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md
new file mode 100644
index 0000000..134ec49
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md
@@ -0,0 +1,23 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Execute ..
+3. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
new file mode 100644
index 0000000..bbcbbe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..968a845
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,7 @@
+| Q | A
+| ------------- | ---
+| Is bugfix? | yes/no
+| New feature? | yes/no
+| Breaks BC? | yes/no
+| Tests pass? | yes/no
+| Fixed issues | comma-separated list of tickets # fixed by the PR, if any
diff --git a/.gitignore b/.gitignore
index 91acb6a..287ef8a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,12 @@ phpunit.phar
/phpcs.xml
+# --------------------------------------------
+# Local PHPMD Config.
+# --------------------------------------------
+/phpmd.xml
+
+
# --------------------------------------------
# IDE Files.
# --------------------------------------------
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..a36db8a
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,23 @@
+# .readthedocs.yml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ configuration: docs/conf.py
+
+# Build documentation with MkDocs
+#mkdocs:
+# configuration: mkdocs.yml
+
+# Optionally build your docs in additional formats such as PDF and ePub
+formats: all
+
+# Optionally set the version of Python and requirements required to build your docs
+python:
+ version: 3.7
+ install:
+ - requirements: docs/requirements.txt
diff --git a/.travis.yml b/.travis.yml
index c085e99..3621a36 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,9 +4,6 @@ os:
- linux
matrix:
- allow_failures:
- - php: hhvm
-
include:
- php: 7.1
- php: 7.2
@@ -18,8 +15,10 @@ before_script:
- travis_retry composer install --no-interaction --prefer-source
script:
- - php vendor/bin/phpcs
- - php vendor/bin/phpunit --coverage-clover=coverage.xml --debug
+ - composer check
+ - composer generate-reports
after_script:
- - bash <(curl -s https://codecov.io/bash)
+ - bash <(curl -s https://codecov.io/bash) -f "build/phpunit/coverage/clover/index.xml"
+ - wget https://scrutinizer-ci.com/ocular.phar
+ - php ocular.phar code-coverage:upload --format=php-clover build/phpunit/coverage/clover/index.xml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5634dba..6993dcd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,49 @@
DotArray Change Log
=====================
+1.1.0 March 02, 2019
+-----------------------------
+- Remove DotArray::uniqueIdentifier
+- Remove DotPathTrait - parts have been moved into DotArray
+- Code Standard Improvements
+- Refactoring DotArray:
+- More Tests.
+
+1.0.5 December 30, 2018
+-----------------------------
+
+- Refactoring DotArray:
+ - Using a Trait (DotFilteringTrait) to split code in more organized units.
+- Refactoring DotPathTrait::flatten
+- using PHPStan.
+- Updating composer.json scripts to use PHPStan.
+- More Tests.
+
+1.0.4 December 30, 2018
+-----------------------------
+
+- Refactoring DotArray:
+ - Using a Trait (DotPathTrait) to split code in more organized units.
+ - Refactor DotArray::mergeRecursive :: less `if ... else` branches.
+ - Refactor DotArray::normalize :: now is recursive and if type of the entry is DotArray then is converted to array.
+ - Apply DotArray::normalize after every DotArray::write used when DotArray::set is called.
+- Fix composer.json `create-folders` script :: in case of fail creating the `build` folder, exit with code 0.
+- Updating README.md
+- Updating Tests
+
+1.0.3 December 28, 2018
+-----------------------------
+
+- Update `.gitattributes`
+
+1.0.2 December 28, 2018
+-----------------------------
+
+- Update README.md
+- Added the following scripts to `composer.json`:
+ - `composer check` (running phpcs & phpunit)
+ - `composer generate-reports` (running phpcs, phpmd, phpunit :: for generating internal reports)
+
1.0.1 December 26, 2018
-----------------------------
diff --git a/README.md b/README.md
index 1dd9ee2..fa542b3 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,26 @@
-# DotArray
+# PHP Dot-Array :: Sail through array using the dot notation
-[](https://github.com/binary-cube/dot-array)
+
+
~ Enjoy your :coffee: ~
Accessing PHP Arrays via DOT notation is easy as:
```php
DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{some.dotted.key}')
```
-[](https://packagist.org/packages/binary-cube/dot-array)
-[](https://packagist.org/packages/binary-cube/dot-array)
-[](https://travis-ci.org/binary-cube/dot-array)
-[](https://www.codacy.com/app/microThread/dot-array)
-[](https://scrutinizer-ci.com/g/binary-cube/dot-array/?branch=master)
-[](https://scrutinizer-ci.com/g/binary-cube/dot-array/?branch=master)
-[](LICENSE)
+[![Minimum PHP Version `PHP >= 7.1`][ico-php-require]][link-php-site]
+[![Latest Stable Version][ico-version]][link-packagist]
+[![Total Downloads][ico-downloads]][link-downloads]
+[![Build Status][ico-travis]][link-travis]
+[![Code Coverage][ico-scrutinizer]][link-scrutinizer]
+[![Quality Score][ico-code-quality]][link-code-quality]
+[![License][ico-license]][link-license]
-----
+
+
+
## Installing
- **via "composer require"**:
@@ -38,6 +42,9 @@ DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{som
}
```
+
+
+
## Usage
>##### REMEMBER: YOU NEED TO KNOW YOUR DATA
@@ -95,6 +102,8 @@ DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{som
```
- **clear** *(empty array <=> [])*:
+ > Set the contents of a given key or keys to the given value (default is empty array).
+
- ```php
$dot->clear('books.{sci-fi & fantasy}');
$dot->clear('books.{sci-fi & fantasy}', null);
@@ -103,7 +112,7 @@ DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{som
// Multiple keys.
$dot->clear([
'books.{sci-fi & fantasy}',
- 'books.{childre\'s books}'
+ 'books.{children\'s books}'
]);
// Vanilla PHP.
@@ -111,30 +120,57 @@ DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{som
```
- **delete** *(unset(...))*:
+ > Delete the given key or keys.
+
- ```php
$dot->delete('books.{sci-fi & fantasy}');
$dot->delete('books.{sci-fi & fantasy}.0.name');
- $dot->delete(['books.{sci-fi & fantasy}.0', 'books.{childre\'s books}.0']);
+ $dot->delete(['books.{sci-fi & fantasy}.0', 'books.{children\'s books}.0']);
+ ```
+
+- **merge**:
+ > Merges one or more arrays into master recursively.
+ If each array has an element with the same string key value, the latter
+ will overwrite the former (different from array_merge_recursive).
+ Recursive merging will be conducted if both arrays have an element of array
+ type and are having the same key.
+ For integer-keyed elements, the elements from the latter array will
+ be appended to the former array.
+
+ - ```php
+ // Example 1.
+ $dot->merge(['key_1' => ['some_key' => 'some_value']]);
+
+ // Example 2.
+ $dot->merge(
+ [
+ 'key_1' => ['some_key' => 'some_value'],
+ ],
+ [
+ 'key_2' => ['some_key' => 'some_value'],
+ ],
+ [
+ 'key_n' => ['some_key' => 'some_value']
+ ],
+ );
```
- **find**:
+ > Find the first item in an array that passes the truth test, otherwise return false.
+ The signature of the callable must be: `function ($value, $key)`.
+
- ```php
- /*
- Find the first item in an array that passes the truth test, otherwise return false
- The signature of the callable must be: `function ($value, $key)`.
- */
- $book = $dot->get('books.{childre\'s books}')->find(function ($value, $key) {
+ $book = $dot->get('books.{children\'s books}')->find(function ($value, $key) {
return $value['price'] > 0;
});
```
- **filter**:
+ > Use a callable function to filter through items.
+ The signature of the callable must be: `function ($value, $key)`
+
- ```php
- /*
- Use a callable function to filter through items.
- The signature of the callable must be: `function ($value, $key)`
- */
- $books = $dot->get('books.{childre\'s books}')->filter(function ($value, $key) {
+ $books = $dot->get('books.{children\'s books}')->filter(function ($value, $key) {
return $value['name'] === 'Harry Potter and the Order of the Phoenix';
});
@@ -159,13 +195,13 @@ DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{som
- [ not-between ]
*/
// Example 1.
- $books = $dot->get('books.{childre\'s books}')->filterBy('price', 'between', 5, 12);
+ $books = $dot->get('books.{children\'s books}')->filterBy('price', 'between', 5, 12);
// Example 2.
- $books = $dot->get('books.{childre\'s books}')->filterBy('price', '>', 10);
+ $books = $dot->get('books.{children\'s books}')->filterBy('price', '>', 10);
// Example 3.
- $books = $dot->get('books.{childre\'s books}')->filterBy('price', 'in', [8.5, 15.49]);
+ $books = $dot->get('books.{children\'s books}')->filterBy('price', 'in', [8.5, 15.49]);
```
- **where**:
@@ -191,20 +227,85 @@ DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{som
*/
// Example 1. (using the signature: [property, comparisonOperator, ...value])
- $books = $dot->get('books.{childre\'s books}')->where(['price', 'between', 5, 12]);
+ $books = $dot->get('books.{children\'s books}')->where(['price', 'between', 5, 12]);
// Example 2. (using the signature: [property, comparisonOperator, ...value])
- $books = $dot->get('books.{childre\'s books}')->where(['price', '>', 10]);
+ $books = $dot->get('books.{children\'s books}')->where(['price', '>', 10]);
// Example 3. (using the signature: [property, comparisonOperator, ...value])
- $books = $dot->get('books.{childre\'s books}')->where(['price', 'in', [8.5, 15.49]]);
+ $books = $dot->get('books.{children\'s books}')->where(['price', 'in', [8.5, 15.49]]);
// Example 4. (using the signature: \Closure)
- $books = $dot->get('books.{childre\'s books}')->where(function ($value, $key) {
+ $books = $dot->get('books.{children\'s books}')->where(function ($value, $key) {
return $value['name'] === 'Harry Potter and the Order of the Phoenix';
});
```
+- **toArray**:
+ > Getting the internal raw array.
+
+ - ```php
+ // Example 1.
+ $dot->toArray();
+
+ // Example 2.
+ $dot->get('books.{sci-fi & fantasy}')->toArray();
+ ```
+
+- **toJson**:
+ > Getting the internal raw array as JSON.
+
+ - ```php
+ // Example 1.
+ $dot->toJson();
+
+ // Example 2.
+ $dot->get('books.{sci-fi & fantasy}')->toJson();
+ ```
+
+- **toFlat**:
+ > Flatten the internal array using the dot delimiter,
+ also the keys are wrapped inside {key} (1 x curly braces).
+
+ - ```php
+ $dot = DotArray::create(
+ [
+ 'a' => [
+ 'b' => 'value',
+ ],
+
+ 'b' => [
+ 1,
+ 2,
+ 3,
+ 'array' => [
+ 1,
+ 2,
+ 3,
+ ]
+ ],
+ ]
+ );
+
+ $dot->toFlat();
+
+ /*
+ The output will be an array:
+ [
+ '{a}.{b}' => 'value',
+ '{b}.{0}' => 1,
+ '{b}.{1}' => 2,
+ '{b}.{2}' => 3,
+ '{b}.{array}.{0}' => 1,
+ '{b}.{array}.{1}' => 2,
+ '{b}.{array}.{2}' => 3,
+ ],
+ */
+ ```
+
+
+
+
### Data Sample:
```php
@@ -236,7 +337,7 @@ $dummyArray = [
],
],
- 'childre\'s books' =>
+ 'children\'s books' =>
[
[
'name' => 'Harry Potter and the Order of the Phoenix',
@@ -269,12 +370,80 @@ $dummyArray = [
];
```
+
+
+
+## Bugs and feature requests
+
+Have a bug or a feature request?
+Please first read the issue guidelines and search for existing and closed issues.
+If your problem or idea is not addressed yet, [please open a new issue][link-new-issue].
+
+
+
+
+## Contributing guidelines
+
+All contributions are more than welcomed.
+Contributions may close an issue, fix a bug (reported or not reported), add new design blocks,
+improve the existing code, add new feature, and so on.
+In the interest of fostering an open and welcoming environment,
+we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone,
+regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality,
+personal appearance, race, religion, or sexual identity and orientation.
+[Read the full Code of Conduct][link-code-of-conduct].
+
+
+
+
+#### Versioning
+
+Through the development of new versions, we're going use the [Semantic Versioning][link-semver].
+
+Example: `1.0.0`.
+- Major release: increment the first digit and reset middle and last digits to zero. Introduces major changes that might break backward compatibility. E.g. 2.0.0
+- Minor release: increment the middle digit and reset last digit to zero. It would fix bugs and also add new features without breaking backward compatibility. E.g. 1.1.0
+- Patch release: increment the third digit. It would fix bugs and keep backward compatibility. E.g. 1.0.1
+
+
+
+
## Authors
* **Banciu N. Cristian Mihai**
-See also the list of [contributors](https://github.com/binary-cube/dot-array/graphs/contributors) who participated in this project.
+See also the list of [contributors][link-contributors] who participated in this project.
+
+
+
## License
-This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
+This project is licensed under the MIT License - see the [LICENSE][link-license] file for details.
+
+
+
+
+
+[ico-php-require]: https://img.shields.io/badge/php-%3E%3D%207.1-8892BF.svg?style=flat-square
+[ico-version]: https://img.shields.io/packagist/v/binary-cube/dot-array.svg?style=flat-square
+[ico-downloads]: https://img.shields.io/packagist/dt/binary-cube/dot-array.svg?style=flat-square
+[ico-travis]: https://img.shields.io/travis/binary-cube/dot-array/master.svg?style=flat-square
+[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/binary-cube/dot-array.svg?style=flat-square
+[ico-code-quality]: https://img.shields.io/scrutinizer/g/binary-cube/dot-array.svg?style=flat-square
+[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
+
+[link-domain]: https://binary-cube.com
+[link-homepage]: https://binary-cube.com
+[link-git-source]: https://github.com/binary-cube/dot-array
+[link-packagist]: https://packagist.org/packages/binary-cube/dot-array
+[link-downloads]: https://packagist.org/packages/binary-cube/dot-array
+[link-php-site]: https://php.net
+[link-semver]: https://semver.org
+[link-code-of-conduct]: https://github.com/binary-cube/dot-array/blob/master/code-of-conduct.md
+[link-license]: https://github.com/binary-cube/dot-array/blob/master/LICENSE
+[link-contributors]: https://github.com/binary-cube/dot-array/graphs/contributors
+[link-new-issue]: https://github.com/binary-cube/dot-array/issues/new
+[link-travis]: https://travis-ci.org/binary-cube/dot-array
+[link-scrutinizer]: https://scrutinizer-ci.com/g/binary-cube/dot-array/code-structure
+[link-code-quality]: https://scrutinizer-ci.com/g/binary-cube/dot-array
diff --git a/composer.json b/composer.json
index 538b1fa..46eff2e 100644
--- a/composer.json
+++ b/composer.json
@@ -1,9 +1,9 @@
{
"name": "binary-cube/dot-array",
- "description": "Navigate through array using JSON Dot notation path.",
+ "description": "PHP Dot-Array :: Sail through array using the dot notation",
"keywords": [
- "DotArray", "dot-array", "array", "dot", "php-array", "php-array-json-path"
+ "DotArray", "dot-array", "array", "dot", "php-array", "php-array-json-path", "php"
],
"type": "library",
@@ -32,7 +32,8 @@
"require-dev": {
"squizlabs/php_codesniffer": "3.*",
"phpunit/phpunit": "~7.0",
- "phpmd/phpmd": "*"
+ "phpmd/phpmd": "*",
+ "phpstan/phpstan": "*"
},
"suggest": {
@@ -40,13 +41,8 @@
"config": {
"sort-packages": true,
- "optimize-autoloader": true
- },
-
- "bin": [
- ],
-
- "scripts": {
+ "optimize-autoloader": true,
+ "process-timeout": 300
},
"autoload": {
@@ -66,5 +62,46 @@
"type": "composer",
"url": "https://asset-packagist.org"
}
- ]
+ ],
+
+ "bin": [
+ ],
+
+ "scripts": {
+ "check": [
+ "@cs-check",
+ "@phpstan",
+ "@tests"
+ ],
+
+ "generate-reports": [
+ "@create-folders",
+ "@cs-report",
+ "@phpstan-report",
+ "@phpmd-report",
+ "@tests-report-html",
+ "@tests-report-xml",
+ "@tests-report-clover"
+ ],
+
+ "create-folders": [
+ "[ ! -d build ] && mkdir -p build || exit 0;"
+ ],
+
+ "cs-check": "phpcs",
+ "cs-fix": "phpcbf",
+ "phpstan": "phpstan analyze src --no-progress",
+ "phpmd": "phpmd src text phpmd.xml.dist",
+ "tests": "phpunit",
+
+ "cs-report": "phpcs --report=json --report-file=build/phpcs-report.json || exit 0;",
+ "phpstan-report": "phpstan analyze src --error-format=checkstyle > build/phpstan-check-style.xml --no-progress || exit 0;",
+ "phpmd-report": "phpmd src xml phpmd.xml.dist --reportfile build/phpmd-report.xml || exit 0;",
+ "tests-report-html": "phpunit --coverage-html build/phpunit/coverage/html || exit 0;",
+ "tests-report-xml": "phpunit --coverage-xml build/phpunit/coverage/xml || exit 0;",
+ "tests-report-clover": "phpunit --coverage-clover build/phpunit/coverage/clover/index.xml || exit 0;"
+ },
+
+ "scripts-descriptions": {
+ }
}
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..fe09287
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,70 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'DotArray'
+copyright = '2019, Banciu N. Cristian Mihai'
+
+# The full version, including alpha/beta/rc tags
+release = '1.0'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinxcontrib.phpdomain'
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ['_build']
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_rtd_theme'
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+
+# -- Custom -------------------------------------------------
+import sphinx_rtd_theme
+html_theme = "sphinx_rtd_theme"
+html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+
+# Set up PHP syntax highlights
+from sphinx.highlighting import lexers
+from pygments.lexers.web import PhpLexer
+
+lexers["php"] = PhpLexer(startinline=True, linenos=1)
+lexers["php-annotations"] = PhpLexer(startinline=True, linenos=1)
+primary_domain = "php"
+
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..662b240
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,62 @@
+.. title:: PHP DotArray Library:: Sail through array using the DOT notation
+
+====================
+DotArray
+====================
+
+.. raw:: html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+DotArray is a simple PHP Library that can access an array in a dotted manner.
+
+- Easy to use.
+- Support to access complex keys that are having dot in the name.
+- Fluent access.
+- Possibility to search & filter items.
+- Dot access can, also, works in the "old school" array way (Vanilla PHP).
+
+.. code-block:: php
+
+ DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{some.dotted.key}')
+
+User Guide
+==========
+
+.. toctree::
+ :maxdepth: 2
+
+ overview
+ quickstart
diff --git a/docs/overview.rst b/docs/overview.rst
new file mode 100644
index 0000000..3915814
--- /dev/null
+++ b/docs/overview.rst
@@ -0,0 +1,108 @@
+========
+Overview
+========
+
+Requirements
+============
+
+#. PHP 7.1
+
+.. _installation:
+
+
+Installation
+============
+
+The recommended way to install DotArray is with
+`Composer `_. Composer is a dependency management tool
+for PHP that allows you to declare the dependencies your project needs and
+installs them into your project.
+
+.. code-block:: bash
+
+ # Install Composer
+ curl -sS https://getcomposer.org/installer | php
+
+You can add DotArray as a dependency using the composer.phar CLI:
+
+.. code-block:: bash
+
+ php composer.phar require binary-cube/dot-array
+
+Alternatively, you can specify DotArray as a dependency in your project's
+existing composer.json file:
+
+.. code-block:: js
+
+ {
+ "require": {
+ "binary-cube/dot-array": "*"
+ }
+ }
+
+After installing, you need to require Composer's autoloader:
+
+.. code-block:: php
+
+ require 'vendor/autoload.php';
+
+You can find out more on how to install Composer, configure autoloading, and
+other best-practices for defining dependencies at `getcomposer.org `_.
+
+
+License
+=======
+
+Licensed using the `MIT license `_.
+
+ Copyright (c) 2018 Binary Cube
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+
+Bugs and feature requests
+=========================
+
+Have a bug or a feature request?
+Please first read the issue guidelines and search for existing and closed issues.
+If your problem or idea is not addressed yet, `please open a new issue `_.
+
+
+Contributing
+============
+
+All contributions are more than welcomed.
+Contributions may close an issue, fix a bug (reported or not reported), add new design blocks,
+improve the existing code, add new feature, and so on.
+In the interest of fostering an open and welcoming environment,
+we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone,
+regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality,
+personal appearance, race, religion, or sexual identity and orientation.
+`Read the full Code of Conduct `_.
+
+
+Versioning
+===========
+
+Through the development of new versions, we're going use the `Semantic Versioning `_.
+
+Example: `1.0.0`.
+- Major release: increment the first digit and reset middle and last digits to zero. Introduces major changes that might break backward compatibility. E.g. 2.0.0
+- Minor release: increment the middle digit and reset last digit to zero. It would fix bugs and also add new features without breaking backward compatibility. E.g. 1.1.0
+- Patch release: increment the third digit. It would fix bugs and keep backward compatibility. E.g. 1.0.1
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/docs/quickstart.rst
@@ -0,0 +1 @@
+
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..855d66c
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,3 @@
+Sphinx>=2.1.0
+sphinx_rtd_theme>=0.4.0
+sphinxcontrib-phpdomain>=0.6.1
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 27d7104..f73483f 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -1,7 +1,7 @@
-
-
+
+
- Base Coding Standards
+ Base Coding Standard
@@ -12,12 +12,12 @@
*/vendor/*
-
+
-
+
@@ -156,12 +156,13 @@
-
-
-
+
+
+
+
@@ -212,8 +213,10 @@
-
-
+
+
+ error
+
@@ -227,9 +230,6 @@
-
-
-
@@ -420,7 +420,7 @@
-->
-
+
diff --git a/phpmd.xml b/phpmd.xml
deleted file mode 100644
index 12f8475..0000000
--- a/phpmd.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
- PHP Mess Detector
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/phpmd.xml.dist b/phpmd.xml.dist
new file mode 100644
index 0000000..4e6ab23
--- /dev/null
+++ b/phpmd.xml.dist
@@ -0,0 +1,148 @@
+
+
+
+ Base PHP Mess Detector Rule Set
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..5f962ab
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,2 @@
+includes:
+ - vendor/phpstan/phpstan/conf/config.levelmax.neon
\ No newline at end of file
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 9bc3bd9..321e6af 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,4 +1,4 @@
-
+
-
- ./tests
+
+ ./tests/Unit
@@ -25,14 +25,4 @@
./src/
-
-
-
-
-
-
-
-
-
-
diff --git a/src/DotArray.php b/src/DotArray.php
index 49d7173..2ee36fb 100644
--- a/src/DotArray.php
+++ b/src/DotArray.php
@@ -18,47 +18,13 @@ class DotArray implements
\Countable
{
- /**
- * Unique object identifier.
- *
- * @var string
- */
- protected $uniqueIdentifier;
+ /* Traits. */
+ use DotFilteringTrait;
- /**
- * Config.
- *
- * @var array
- */
- protected $config = [
- 'path' => [
- 'template' => '#(?|(?|[](.*?)[])|(.*?))(?:$|\.+)#i',
- 'wildcards' => [
- '' => ['\'', '\"', '\[', '\(', '\{'],
- '' => ['\'', '\"', '\]', '\)', '\}'],
- ],
- ],
- ];
-
- /**
- * The pattern that allow to match the JSON paths that use the dot notation.
- *
- * Allowed tokens for more complex paths: '', "", [], (), {}
- * Examples:
- *
- * - foo.bar
- * - foo.'bar'
- * - foo."bar"
- * - foo.[bar]
- * - foo.(bar)
- * - foo.{bar}
- *
- * Or more complex:
- * - foo.{bar}.[component].{version.1.0}
- *
- * @var string
- */
- protected $nestedPathPattern;
+ private const TEMPLATE_PATTERN = '#(?|(?|[%s](.*?)[%s])|(.*?))(?:$|\.+)#i';
+ private const WRAP_KEY = '{%s}';
+ private const TOKEN_START = ['\'', '\"', '\[', '\(', '\{'];
+ private const TOKEN_END = ['\'', '\"', '\]', '\)', '\}'];
/**
* Stores the original data.
@@ -67,7 +33,6 @@ class DotArray implements
*/
protected $items;
-
/**
* Creates an DotArray object.
*
@@ -80,7 +45,6 @@ public static function create($items)
return (new static($items));
}
-
/**
* @param string $json
*
@@ -91,199 +55,37 @@ public static function createFromJson($json)
return static::create(\json_decode($json, true));
}
-
/**
- * Return the given items as an array
- *
- * @param mixed $items
+ * Getting the path pattern.
*
- * @return array
- */
- protected static function normalize(&$items)
- {
- if (\is_array($items)) {
- return $items;
- } else if (empty($items)) {
- return [];
- } else if ($items instanceof self) {
- return $items->toArray();
- }
-
- return (array) $items;
- }
-
-
- /**
- * List with internal operators and the associated callbacks.
- *
- * @return array
- */
- protected static function operators()
- {
- return [
- [
- 'tokens' => ['=', '==', 'eq'],
- 'closure' => function ($item, $property, $value) {
- return $item[$property] == $value[0];
- },
- ],
-
- [
- 'tokens' => ['===', 'i'],
- 'closure' => function ($item, $property, $value) {
- return $item[$property] === $value[0];
- },
- ],
-
- [
- 'tokens' => ['!=', 'ne'],
- 'closure' => function ($item, $property, $value) {
- return $item[$property] != $value[0];
- },
- ],
-
- [
- 'tokens' => ['!==', 'ni'],
- 'closure' => function ($item, $property, $value) {
- return $item[$property] !== $value[0];
- },
- ],
-
- [
- 'tokens' => ['<', 'lt'],
- 'closure' => function ($item, $property, $value) {
- return $item[$property] < $value[0];
- },
- ],
-
- [
- 'tokens' => ['>', 'gt'],
- 'closure' => function ($item, $property, $value) {
- return $item[$property] > $value[0];
- },
- ],
-
- [
- 'tokens' => ['<=', 'lte'],
- 'closure' => function ($item, $property, $value) {
- return $item[$property] <= $value[0];
- },
- ],
-
- [
- 'tokens' => ['>=', 'gte'],
- 'closure' => function ($item, $property, $value) {
- return $item[$property] >= $value[0];
- },
- ],
-
- [
- 'tokens' => ['in', 'contains'],
- 'closure' => function ($item, $property, $value) {
- return \in_array($item[$property], (array) $value, true);
- },
- ],
-
- [
- 'tokens' => ['not-in', 'not-contains'],
- 'closure' => function ($item, $property, $value) {
- return !\in_array($item[$property], (array) $value, true);
- },
- ],
-
- [
- 'tokens' => ['between'],
- 'closure' => function ($item, $property, $value) {
- return ($item[$property] >= $value[0] && $item[$property] <= $value[1]);
- },
- ],
-
- [
- 'tokens' => ['not-between'],
- 'closure' => function ($item, $property, $value) {
- return ($item[$property] < $value[0] || $item[$property] > $value[1]);
- },
- ],
- ];
- }
-
-
- /**
- * DotArray Constructor.
+ * Allowed tokens for more complex paths: '', "", [], (), {}
+ * Examples:
*
- * @param mixed $items
- */
- public function __construct($items = [])
- {
- $this->items = static::normalize($items);
- }
-
-
- /**
- * DotArray Destructor.
- */
- public function __destruct()
- {
- unset($this->uniqueIdentifier);
- unset($this->items);
- unset($this->nestedPathPattern);
- }
-
-
- /**
- * Call object as function.
+ * - foo.bar
+ * - foo.'bar'
+ * - foo."bar"
+ * - foo.[bar]
+ * - foo.(bar)
+ * - foo.{bar}
*
- * @param null|string $key
+ * Or more complex:
+ * - foo.{bar}.[component].{version.1.0}
*
- * @return mixed|static
- */
- public function __invoke($key = null)
- {
- return $this->get($key);
- }
-
-
- /**
* @return string
*/
- public function uniqueIdentifier()
+ protected static function pathPattern()
{
- if (empty($this->uniqueIdentifier)) {
- $this->uniqueIdentifier = vsprintf(
- '{%s}.{%s}.{%s}',
+ return (
+ vsprintf(
+ self::TEMPLATE_PATTERN,
[
- static::class,
- \uniqid('', true),
- microtime(true),
+ \implode('', self::TOKEN_START),
+ \implode('', self::TOKEN_END),
]
- );
- }
-
- return $this->uniqueIdentifier;
- }
-
-
- /**
- * Getting the nested path pattern.
- *
- * @return string
- */
- protected function nestedPathPattern()
- {
- if (empty($this->nestedPathPattern)) {
- $path = $this->config['path']['template'];
-
- foreach ($this->config['path']['wildcards'] as $wildcard => $tokens) {
- $path = \str_replace($wildcard, \implode('', $tokens), $path);
- }
-
- $this->nestedPathPattern = $path;
- }
-
- return $this->nestedPathPattern;
+ )
+ );
}
-
/**
* Converts dot string path to segments.
*
@@ -291,13 +93,17 @@ protected function nestedPathPattern()
*
* @return array
*/
- protected function pathToSegments($path)
+ final protected static function pathToSegments($path)
{
$path = \trim($path, " \t\n\r\0\x0B\.");
$segments = [];
$matches = [];
- \preg_match_all($this->nestedPathPattern(), $path, $matches);
+ if (\mb_strlen($path, 'UTF-8') === 0) {
+ return [];
+ }
+
+ \preg_match_all(self::pathPattern(), $path, $matches);
if (!empty($matches[1])) {
$matches = $matches[1];
@@ -310,12 +116,11 @@ function ($match) {
);
}
- unset($matches);
+ unset($path, $matches);
return (empty($segments) ? [] : $segments);
}
-
/**
* Wrap a given string into special characters.
*
@@ -323,30 +128,82 @@ function ($match) {
*
* @return string
*/
- protected function wrapSegmentKey($key)
+ final protected static function wrapSegmentKey($key)
{
- return "{{~$key~}}";
+ return \vsprintf(self::WRAP_KEY, [$key]);
}
-
/**
* @param array $segments
*
* @return string
*/
- protected function segmentsToKey(array $segments)
+ final protected static function segmentsToKey(array $segments)
{
return (
\implode(
- '',
+ '.',
\array_map(
- [$this, 'wrapSegmentKey'],
+ function ($segment) {
+ return self::wrapSegmentKey($segment);
+ },
$segments
)
)
);
}
+ /**
+ * Flatten the internal array using the dot delimiter,
+ * also the keys are wrapped inside {key} (1 x curly braces).
+ *
+ * @param array $items
+ * @param array $prepend
+ *
+ * @return array
+ */
+ final protected static function flatten(array $items, $prepend = [])
+ {
+ $flatten = [];
+
+ foreach ($items as $key => $value) {
+ if (\is_array($value) && !empty($value)) {
+ $flatten = \array_merge($flatten, self::flatten($value, \array_merge($prepend, [$key])));
+ continue;
+ }
+
+ $segmentsToKey = self::segmentsToKey(\array_merge($prepend, [$key]));
+
+ $flatten[$segmentsToKey] = $value;
+ }
+
+ return $flatten;
+ }
+
+ /**
+ * Return the given items as an array
+ *
+ * @param mixed $items
+ *
+ * @return array
+ */
+ final protected static function normalize($items)
+ {
+ if ($items instanceof self) {
+ $items = $items->toArray();
+ }
+
+ if (\is_array($items)) {
+ foreach ($items as $k => $v) {
+ if (\is_array($v) || $v instanceof self) {
+ $v = self::normalize($v);
+ }
+ $items[$k] = $v;
+ }
+ }
+
+ return (array) $items;
+ }
/**
* @param array|DotArray|mixed $array1
@@ -354,38 +211,58 @@ protected function segmentsToKey(array $segments)
*
* @return array
*/
- protected static function mergeRecursive($array1, $array2 = null)
+ final protected static function mergeRecursive($array1, $array2 = null)
{
- $args = \func_get_args();
+ $args = self::normalize(\func_get_args());
$res = \array_shift($args);
while (!empty($args)) {
foreach (\array_shift($args) as $k => $v) {
- if ($v instanceof self) {
- $v = $v->toArray();
+ if (\is_int($k) && \array_key_exists($k, $res)) {
+ $res[] = $v;
+ continue;
}
- if (\is_int($k)) {
- if (\array_key_exists($k, $res)) {
- $res[] = $v;
- } else {
- $res[$k] = $v;
- }
- } else if (
- \is_array($v)
- && isset($res[$k])
- && \is_array($res[$k])
- ) {
- $res[$k] = static::mergeRecursive($res[$k], $v);
- } else {
- $res[$k] = $v;
+ if (\is_array($v) && isset($res[$k]) && \is_array($res[$k])) {
+ $v = self::mergeRecursive($res[$k], $v);
}
- }//end foreach
- }//end while
+
+ $res[$k] = $v;
+ }
+ }
return $res;
}
+ /**
+ * DotArray Constructor.
+ *
+ * @param mixed $items
+ */
+ public function __construct($items = [])
+ {
+ $this->items = self::normalize($items);
+ }
+
+ /**
+ * DotArray Destructor.
+ */
+ public function __destruct()
+ {
+ unset($this->items);
+ }
+
+ /**
+ * Call object as function.
+ *
+ * @param null|string $key
+ *
+ * @return mixed|static
+ */
+ public function __invoke($key = null)
+ {
+ return $this->get($key);
+ }
/**
* Merges one or more arrays into master recursively.
@@ -407,42 +284,36 @@ public function merge($array)
[
$this, 'mergeRecursive',
],
- \array_merge(
- [$this->items],
- \func_get_args()
- )
+ \array_values(\array_merge([$this->items], \func_get_args()))
);
return $this;
}
-
/**
- * @param string $key
- * @param mixed $default
+ * @param string|null|mixed $key
+ * @param mixed $default
*
* @return array|mixed
*/
- protected function &read($key, $default)
+ protected function &read($key = null, $default = null)
{
- $segments = $this->pathToSegments($key);
+ $segments = self::pathToSegments($key);
$items = &$this->items;
foreach ($segments as $segment) {
- if (
- !\is_array($items)
- || !\array_key_exists($segment, $items)
- ) {
+ if (!\is_array($items) || !\array_key_exists($segment, $items)) {
return $default;
}
$items = &$items[$segment];
}
+ unset($segments);
+
return $items;
}
-
/**
* @param string $key
* @param mixed $value
@@ -451,7 +322,7 @@ protected function &read($key, $default)
*/
protected function write($key, $value)
{
- $segments = $this->pathToSegments($key);
+ $segments = self::pathToSegments($key);
$count = \count($segments);
$items = &$this->items;
@@ -459,10 +330,7 @@ protected function write($key, $value)
$segment = $segments[$i];
if (
- (
- !isset($items[$segment])
- || !\is_array($items[$segment])
- )
+ (!isset($items[$segment]) || !\is_array($items[$segment]))
&& ($i < ($count - 1))
) {
$items[$segment] = [];
@@ -471,9 +339,16 @@ protected function write($key, $value)
$items = &$items[$segment];
}
+ if (\is_array($value) || $value instanceof self) {
+ $value = self::normalize($value);
+ }
+
$items = $value;
- }
+ if (!\is_array($this->items)) {
+ $this->items = self::normalize($this->items);
+ }
+ }
/**
* Delete the given key or keys.
@@ -484,19 +359,17 @@ protected function write($key, $value)
*/
protected function remove($key)
{
- $segments = $this->pathToSegments($key);
+ $segments = self::pathToSegments($key);
$count = \count($segments);
$items = &$this->items;
for ($i = 0; $i < $count; $i++) {
$segment = $segments[$i];
- // Nothing to unset.
if (!\array_key_exists($segment, $items)) {
break;
}
- // Last item, time to unset.
if ($i === ($count - 1)) {
unset($items[$segment]);
break;
@@ -506,7 +379,6 @@ protected function remove($key)
}
}
-
/**
* @param string $key
*
@@ -514,12 +386,13 @@ protected function remove($key)
*/
public function has($key)
{
- return ($this->read($key, $this->uniqueIdentifier()) !== $this->uniqueIdentifier());
- }
+ $identifier = \uniqid(static::class, true);
+ return ($identifier !== $this->read($key, $identifier));
+ }
/**
- * Check if a given key is empty.
+ * Check if a given key contains empty values (null, [], 0, false)
*
* @param null|string $key
*
@@ -527,20 +400,9 @@ public function has($key)
*/
public function isEmpty($key = null)
{
- if (!isset($key)) {
- return empty($this->items);
- }
-
- $items = $this->read($key, null);
-
- if ($items instanceof self) {
- $items = $items->toArray();
- }
-
- return empty($items);
+ return empty($this->read($key, null));
}
-
/**
* @param null|string $key
* @param null|mixed $default
@@ -558,18 +420,17 @@ public function get($key = null, $default = null)
return $items;
}
-
/**
* Set the given value to the provided key or keys.
*
- * @param string|array $keys
- * @param mixed $value
+ * @param null|string|array $keys
+ * @param mixed|mixed $value
*
* @return static
*/
- public function set($keys, $value)
+ public function set($keys = null, $value = [])
{
- $keys = (array) $keys;
+ $keys = (array) (!isset($keys) ? [$keys] : $keys);
foreach ($keys as $key) {
$this->write($key, $value);
@@ -578,7 +439,6 @@ public function set($keys, $value)
return $this;
}
-
/**
* Delete the given key or keys.
*
@@ -597,12 +457,11 @@ public function delete($keys)
return $this;
}
-
/**
* Set the contents of a given key or keys to the given value (default is empty array).
*
* @param null|string|array $keys
- * @param array $value
+ * @param array|mixed $value
*
* @return static
*/
@@ -617,183 +476,17 @@ public function clear($keys = null, $value = [])
return $this;
}
-
- /**
- * Find the first item in an array that passes the truth test, otherwise return false
- * The signature of the callable must be: `function ($value, $key)`
- *
- * @param \Closure $closure
- *
- * @return false|mixed
- */
- public function find(\Closure $closure)
- {
- foreach ($this->items as $key => $value) {
- if ($closure($value, $key)) {
- if (\is_array($value)) {
- $value = static::create($value);
- }
-
- return $value;
- }
- }
-
- return false;
- }
-
-
- /**
- * Use a callable function to filter through items.
- * The signature of the callable must be: `function ($value, $key)`
- *
- * @param \Closure|null $closure
- * @param int $flag Flag determining what arguments are sent to callback.
- * ARRAY_FILTER_USE_KEY :: pass key as the only argument
- * to callback.
- * ARRAY_FILTER_USE_BOTH :: pass both value
- * and key as arguments to callback.
- *
- * @return static
- */
- public function filter(\Closure $closure = null, $flag = ARRAY_FILTER_USE_BOTH)
- {
- $items = $this->items;
-
- if (!isset($closure)) {
- return static::create($items);
- }
-
- return (
- static::create(
- \array_values(
- \array_filter(
- $items,
- $closure,
- $flag
- )
- )
- )
- );
- }
-
-
- /**
- * Allow to filter an array using one of the following comparison operators:
- * - [ =, ==, eq (equal) ]
- * - [ ===, i (identical) ]
- * - [ !=, ne (not equal) ]
- * - [ !==, ni (not identical) ]
- * - [ <, lt (less than) ]
- * - [ >, gr (greater than) ]
- * - [ <=, lte (less than or equal to) ]
- * - [ =>, gte (greater than or equal to) ]
- * - [ in, contains ]
- * - [ not-in, not-contains ]
- * - [ between ]
- * - [ not-between ]
- *
- * @param string $property
- * @param string $comparisonOperator
- * @param mixed $value
- *
- * @return static
- */
- public function filterBy($property, $comparisonOperator, $value)
- {
- $args = \func_get_args();
- $value = (array) (array_slice($args, 2, count($args)));
-
- $closure = null;
- $operators = static::operators();
-
- if (isset($value[0]) && \is_array($value[0])) {
- $value = $value[0];
- }
-
- foreach ($operators as $entry) {
- if (\in_array($comparisonOperator, $entry['tokens'])) {
- $closure = function ($item) use ($entry, $property, $value) {
- $item = (array) $item;
-
- if (!array_key_exists($property, $item)) {
- return false;
- }
-
- return $entry['closure']($item, $property, $value);
- };
-
- break;
- }
- }
-
- return $this->filter($closure);
- }
-
-
- /**
- * Filtering through array.
- * The signature of the call can be:
- * - where([property, comparisonOperator, ...value])
- * - where(\Closure) :: The signature of the callable must be: `function ($value, $key)`
- * - where([\Closure]) :: The signature of the callable must be: `function ($value, $key)`
- *
- * Allowed comparison operators:
- * - [ =, ==, eq (equal) ]
- * - [ ===, i (identical) ]
- * - [ !=, ne (not equal) ]
- * - [ !==, ni (not identical) ]
- * - [ <, lt (less than) ]
- * - [ >, gr (greater than) ]
- * - [ <=, lte (less than or equal to) ]
- * - [ =>, gte (greater than or equal to) ]
- * - [ in, contains ]
- * - [ not-in, not-contains ]
- * - [ between ]
- * - [ not-between ]
- *
- * @param array|callable $criteria
- *
- * @return static
- */
- public function where($criteria)
- {
- $criteria = (array) $criteria;
-
- if (empty($criteria)) {
- return $this->filter();
- }
-
- $closure = \array_shift($criteria);
-
- if ($closure instanceof \Closure) {
- return $this->filter($closure);
- }
-
- $property = $closure;
- $comparisonOperator = \array_shift($criteria);
- $value = $criteria;
-
- if (isset($value[0]) && \is_array($value[0])) {
- $value = $value[0];
- }
-
- return $this->filterBy($property, $comparisonOperator, $value);
- }
-
-
/**
* Returning the first value from the current array.
+ * False otherwise, in case the list is empty.
*
* @return mixed
*/
public function first()
{
- $items = $this->items;
-
- return \array_shift($items);
+ return \reset($this->items);
}
-
/**
* Whether a offset exists
*
@@ -811,7 +504,6 @@ public function offsetExists($offset)
return $this->has($offset);
}
-
/**
* Offset to retrieve
*
@@ -828,7 +520,6 @@ public function &offsetGet($offset)
return $this->read($offset, null);
}
-
/**
* Offset to set
*
@@ -846,7 +537,6 @@ public function offsetSet($offset, $value)
$this->write($offset, $value);
}
-
/**
* Offset to unset
*
@@ -868,7 +558,6 @@ public function offsetUnset($offset)
$this->remove($offset);
}
-
/**
* Count elements of an object
*
@@ -885,27 +574,6 @@ public function count($mode = COUNT_NORMAL)
return \count($this->items, $mode);
}
-
- /**
- * @return array
- */
- public function toArray()
- {
- return $this->items;
- }
-
-
- /**
- * @param int $options
- *
- * @return string
- */
- public function toJson($options = 0)
- {
- return \json_encode($this->items, $options);
- }
-
-
/**
* Specify data which should be serialized to JSON
*
@@ -921,7 +589,6 @@ public function jsonSerialize()
return $this->items;
}
-
/**
* String representation of object
*
@@ -936,7 +603,6 @@ public function serialize()
return \serialize($this->items);
}
-
/**
* Constructs the object
*
@@ -953,7 +619,6 @@ public function unserialize($serialized)
$this->items = \unserialize($serialized);
}
-
/**
* Retrieve an external iterator.
*
@@ -968,5 +633,37 @@ public function getIterator()
return new \ArrayIterator($this->items);
}
+ /**
+ * Getting the internal raw array.
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ return $this->items;
+ }
+
+ /**
+ * Getting the internal raw array as JSON.
+ *
+ * @param int $options
+ *
+ * @return string
+ */
+ public function toJson($options = 0)
+ {
+ return (string) \json_encode($this->items, $options);
+ }
+
+ /**
+ * Flatten the internal array using the dot delimiter,
+ * also the keys are wrapped inside {key} (1 x curly braces).
+ *
+ * @return array
+ */
+ public function toFlat()
+ {
+ return self::flatten($this->items);
+ }
}
diff --git a/src/DotFilteringTrait.php b/src/DotFilteringTrait.php
new file mode 100644
index 0000000..6e55514
--- /dev/null
+++ b/src/DotFilteringTrait.php
@@ -0,0 +1,266 @@
+
+ * @license https://github.com/binary-cube/dot-array/blob/master/LICENSE
+ * @link https://github.com/binary-cube/dot-array
+ */
+trait DotFilteringTrait
+{
+
+ /**
+ * List with internal operators and the associated callbacks.
+ *
+ * @return array
+ */
+ protected static function operators()
+ {
+ return [
+ [
+ 'tokens' => ['=', '==', 'eq'],
+ 'closure' => function ($item, $property, $value) {
+ return $item[$property] == $value[0];
+ },
+ ],
+
+ [
+ 'tokens' => ['===', 'i'],
+ 'closure' => function ($item, $property, $value) {
+ return $item[$property] === $value[0];
+ },
+ ],
+
+ [
+ 'tokens' => ['!=', 'ne'],
+ 'closure' => function ($item, $property, $value) {
+ return $item[$property] != $value[0];
+ },
+ ],
+
+ [
+ 'tokens' => ['!==', 'ni'],
+ 'closure' => function ($item, $property, $value) {
+ return $item[$property] !== $value[0];
+ },
+ ],
+
+ [
+ 'tokens' => ['<', 'lt'],
+ 'closure' => function ($item, $property, $value) {
+ return $item[$property] < $value[0];
+ },
+ ],
+
+ [
+ 'tokens' => ['>', 'gt'],
+ 'closure' => function ($item, $property, $value) {
+ return $item[$property] > $value[0];
+ },
+ ],
+
+ [
+ 'tokens' => ['<=', 'lte'],
+ 'closure' => function ($item, $property, $value) {
+ return $item[$property] <= $value[0];
+ },
+ ],
+
+ [
+ 'tokens' => ['>=', 'gte'],
+ 'closure' => function ($item, $property, $value) {
+ return $item[$property] >= $value[0];
+ },
+ ],
+
+ [
+ 'tokens' => ['in', 'contains'],
+ 'closure' => function ($item, $property, $value) {
+ return \in_array($item[$property], (array) $value, true);
+ },
+ ],
+
+ [
+ 'tokens' => ['not-in', 'not-contains'],
+ 'closure' => function ($item, $property, $value) {
+ return !\in_array($item[$property], (array) $value, true);
+ },
+ ],
+
+ [
+ 'tokens' => ['between'],
+ 'closure' => function ($item, $property, $value) {
+ return ($item[$property] >= $value[0] && $item[$property] <= $value[1]);
+ },
+ ],
+
+ [
+ 'tokens' => ['not-between'],
+ 'closure' => function ($item, $property, $value) {
+ return ($item[$property] < $value[0] || $item[$property] > $value[1]);
+ },
+ ],
+ ];
+ }
+
+ /**
+ * Find the first item in an array that passes the truth test, otherwise return false.
+ * The signature of the callable must be: `function ($value, $key)`
+ *
+ * @param \Closure $closure
+ *
+ * @return false|mixed
+ */
+ public function find(\Closure $closure)
+ {
+ foreach ($this->items as $key => $value) {
+ if ($closure($value, $key)) {
+ if (\is_array($value)) {
+ $value = static::create($value);
+ }
+
+ return $value;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Use a callable function to filter through items.
+ * The signature of the callable must be: `function ($value, $key)`
+ *
+ * @param \Closure|null $closure
+ * @param int $flag Flag determining what arguments are sent to callback.
+ * ARRAY_FILTER_USE_KEY :: pass key as the only argument
+ * to callback. ARRAY_FILTER_USE_BOTH :: pass both value
+ * and key as arguments to callback.
+ *
+ * @return static
+ */
+ public function filter(\Closure $closure = null, $flag = ARRAY_FILTER_USE_BOTH)
+ {
+ $items = $this->items;
+
+ if (!isset($closure)) {
+ return static::create($items);
+ }
+
+ return static::create(
+ \array_values(
+ \array_filter(
+ $items,
+ $closure,
+ $flag
+ )
+ )
+ );
+ }
+
+ /**
+ * Allow to filter an array using one of the following comparison operators:
+ * - [ =, ==, eq (equal) ]
+ * - [ ===, i (identical) ]
+ * - [ !=, ne (not equal) ]
+ * - [ !==, ni (not identical) ]
+ * - [ <, lt (less than) ]
+ * - [ >, gr (greater than) ]
+ * - [ <=, lte (less than or equal to) ]
+ * - [ =>, gte (greater than or equal to) ]
+ * - [ in, contains ]
+ * - [ not-in, not-contains ]
+ * - [ between ]
+ * - [ not-between ]
+ *
+ * @param string $property
+ * @param string $comparisonOperator
+ * @param mixed $value
+ *
+ * @return static
+ */
+ public function filterBy($property, $comparisonOperator, $value)
+ {
+ $args = \func_get_args();
+ $value = \array_slice($args, 2, \count($args));
+
+ $closure = null;
+ $operators = static::operators();
+
+ if (isset($value[0]) && \is_array($value[0])) {
+ $value = $value[0];
+ }
+
+ foreach ($operators as $entry) {
+ if (\in_array($comparisonOperator, $entry['tokens'])) {
+ $closure = function ($item) use ($entry, $property, $value) {
+ $item = (array) $item;
+
+ if (!\array_key_exists($property, $item)) {
+ return false;
+ }
+
+ return $entry['closure']($item, $property, $value);
+ };
+
+ break;
+ }
+ }
+
+ return $this->filter($closure);
+ }
+
+ /**
+ * Filtering through array.
+ * The signature of the call can be:
+ * - where([property, comparisonOperator, ...value])
+ * - where(\Closure) :: The signature of the callable must be: `function ($value, $key)`
+ * - where([\Closure]) :: The signature of the callable must be: `function ($value, $key)`
+ *
+ * Allowed comparison operators:
+ * - [ =, ==, eq (equal) ]
+ * - [ ===, i (identical) ]
+ * - [ !=, ne (not equal) ]
+ * - [ !==, ni (not identical) ]
+ * - [ <, lt (less than) ]
+ * - [ >, gr (greater than) ]
+ * - [ <=, lte (less than or equal to) ]
+ * - [ =>, gte (greater than or equal to) ]
+ * - [ in, contains ]
+ * - [ not-in, not-contains ]
+ * - [ between ]
+ * - [ not-between ]
+ *
+ * @param array|callable $criteria
+ *
+ * @return static
+ */
+ public function where($criteria)
+ {
+ $criteria = (array) $criteria;
+
+ if (empty($criteria)) {
+ return $this->filter();
+ }
+
+ $closure = \array_shift($criteria);
+
+ if ($closure instanceof \Closure) {
+ return $this->filter($closure);
+ }
+
+ $property = $closure;
+ $comparisonOperator = \array_shift($criteria);
+ $value = $criteria;
+
+ if (isset($value[0]) && \is_array($value[0])) {
+ $value = $value[0];
+ }
+
+ return $this->filterBy($property, $comparisonOperator, $value);
+ }
+
+}
diff --git a/tests/Integration/ArrayDataProvider.php b/tests/Unit/ArrayDataProvider.php
similarity index 98%
rename from tests/Integration/ArrayDataProvider.php
rename to tests/Unit/ArrayDataProvider.php
index 77dbaea..129a3ba 100644
--- a/tests/Integration/ArrayDataProvider.php
+++ b/tests/Unit/ArrayDataProvider.php
@@ -1,6 +1,6 @@
[
+ 'b' => [
+ 'c' => true,
+ ],
+ ],
+ ];
+
+ return [
+ [
+ 'path' => 'a.b.c',
+ 'array' => $array,
+ ],
+ [
+ 'path' => "'a'.'b'.'c'",
+ 'array' => $array,
+ ],
+ [
+ 'path' => '"a"."b"."c"',
+ 'array' => $array,
+ ],
+ [
+ 'path' => '[a].[b].[c]',
+ 'array' => $array,
+ ],
+ [
+ 'path' => '(a).(b).(c)',
+ 'array' => $array,
+ ],
+ [
+ 'path' => '{a}.{b}.{c}',
+ 'array' => $array,
+ ],
+ [
+ 'path' => '[a].(b).{c}',
+ 'array' => $array,
+ ],
+ ];
+ }
+
+ /**
+ * Testing different patterns.
+ *
+ * @param string $path The query pattern
+ * @param array $array The array
+ *
+ * @covers \BinaryCube\DotArray\DotArray::get
+ * @covers \BinaryCube\DotArray\DotArray::toArray
+ * @covers \BinaryCube\DotArray\DotArray::
+ * @covers \BinaryCube\DotArray\DotArray::
+ *
+ * @dataProvider tokens
+ *
+ * @return void
+ */
+ public function testTokens($path, $array)
+ {
+ $dot = DotArray::create($array);
+
+ self::assertInstanceOf(DotArray::class, $dot);
+ self::assertTrue($dot->get($path));
+ }
/**
* Testing the Get Method.
@@ -103,7 +168,6 @@ public function testGet()
self::assertIsString($this->dot['mixed_array.hello-world.{Nǐ hǎo}']);
}
-
/**
* Testing the Set Method.
*
@@ -122,16 +186,25 @@ public function testSet()
],
];
- $this->dot['a']['b']['b'] = 2;
+ $this->dot['a']['b']['b'] = 2;
+ $this->dot['a']['b']['obj'] = [
+ DotArray::create(['key' => 'was a dot object 1']),
+ DotArray::create(['key' => 'was a dot object 2']),
+ ];
$this->dot->set('mixed_array.{new-key}', []);
$this->dot->set('mixed_array.{👋.🤘.some-key}', []);
$this->dot->set('a.b.a', 1);
$this->dot->set('new1.new2', 1);
+ $this->dot->set('{dot.obj}', DotArray::create('some_value'));
self::assertIsArray($this->dot->get('mixed_array.{new-key}')->toArray());
self::assertEmpty($this->dot->get('mixed_array.{👋.🤘.some-key}')->toArray());
+ self::assertIsArray($this->dot->get('{dot.obj}')->toArray());
+ self::assertIsArray($this->dot->get('a.b.obj')->toArray());
+ self::assertIsArray($this->dot->get('a.b.obj.0')->toArray());
+
self::assertIsInt($this->dot->get('a.b.a'));
self::assertIsInt($this->dot['a']['b']['a']);
self::assertIsInt($this->dot['a.b.a']);
@@ -147,8 +220,9 @@ public function testSet()
self::assertIsInt($this->dot->get('new1.new2'));
self::assertIsInt($this->dot['new1']['new2']);
self::assertIsInt($this->dot['new1.new2']);
- }
+ self::assertSame([], $this->dot->set(null, null)->toArray());
+ }
/**
* Testing the Has Method.
@@ -179,7 +253,6 @@ public function testHas()
self::assertNotTrue($this->dot->has('a.b.c.d'));
}
-
/**
* Testing the isEmpty Method.
*
@@ -209,7 +282,6 @@ public function testIsEmpty()
self::assertIsBool($this->dot->get('dotObject')->isEmpty());
}
-
/**
* Testing the Delete Method.
*
@@ -244,7 +316,6 @@ public function testDelete()
self::assertTrue(array_key_exists('one', $this->dot['assoc_array']));
}
-
/**
* Testing the Clear Method.
*
@@ -272,7 +343,6 @@ public function testClear()
self::assertEmpty($users->toArray());
}
-
/**
* Testing the Merge Method.
*
@@ -332,14 +402,38 @@ public function testMerge()
]
];
- $this->dot->merge($extraData);
+ $this->dot->merge(
+ $extraData,
+ [
+ 'new-entry' => [
+ 'c' => new DotArray(
+ [
+ 'd' => [1],
+ ]
+ ),
+ ],
+ ],
+ [
+ 'new-entry' => [
+ 'c' => new DotArray(
+ [
+ 'd' => [1],
+ ]
+ ),
+ ],
+ ]
+ );
+
self::assertIsArray($this->dot->get('{new-entry}.a')->toArray());
+ self::assertIsArray($this->dot->get('{new-entry}.b')->toArray());
+ self::assertIsArray($this->dot->get('{new-entry}.b.c')->toArray());
+ self::assertIsArray($this->dot->get('{new-entry}.b.c.e2')->toArray());
+
self::assertEquals('new value for assoc_array.one.element_1', $this->dot->get('assoc_array.one.element_1'));
self::assertEquals(['.other.config' => ['port' => 9300]], $this->dot->get('mixed_array.{👋.🤘.some-key}.config.{elastic-search}.{v6.0}')->toArray());
self::assertCount(3, $this->dot->get('mixed_array.{👋.🤘.some-key}.config.memcached.servers'));
}
-
/**
* Testing the Count Method.
*
@@ -360,10 +454,10 @@ public function testCount()
self::assertEquals(1, count($this->dot['assoc_array']['three']));
}
-
/**
* Testing the Find Method.
*
+ * @covers \BinaryCube\DotArray\DotFilteringTrait
* @covers \BinaryCube\DotArray\DotArray::find
* @covers \BinaryCube\DotArray\DotArray::
* @covers \BinaryCube\DotArray\DotArray::
@@ -399,10 +493,10 @@ function () {
);
}
-
/**
* Testing the Filter Method.
*
+ * @covers \BinaryCube\DotArray\DotFilteringTrait
* @covers \BinaryCube\DotArray\DotArray::filter
* @covers \BinaryCube\DotArray\DotArray::
* @covers \BinaryCube\DotArray\DotArray::
@@ -423,10 +517,10 @@ function ($value) {
self::assertSame([1, 2, 3, 4], $under->toArray());
}
-
/**
* Testing the FilterBy Method.
*
+ * @covers \BinaryCube\DotArray\DotFilteringTrait
* @covers \BinaryCube\DotArray\DotArray::filterBy
* @covers \BinaryCube\DotArray\DotArray::
* @covers \BinaryCube\DotArray\DotArray::
@@ -533,10 +627,10 @@ function ($value) {
);
}
-
/**
* Testing the Where Method.
*
+ * @covers \BinaryCube\DotArray\DotFilteringTrait
* @covers \BinaryCube\DotArray\DotArray::where
* @covers \BinaryCube\DotArray\DotArray::
* @covers \BinaryCube\DotArray\DotArray::
@@ -606,7 +700,6 @@ function ($value) {
self::assertSame($user1, $users->toArray());
}
-
/**
* Testing the First Method.
*
@@ -624,6 +717,20 @@ public function testFirst()
);
}
+ /**
+ * Testing the toArray Method.
+ *
+ * @covers \BinaryCube\DotArray\DotArray::toArray
+ * @covers \BinaryCube\DotArray\DotArray::
+ * @covers \BinaryCube\DotArray\DotArray::
+ *
+ * @return void
+ */
+ public function testToArray()
+ {
+ self::assertIsArray($this->dot->toArray());
+ self::assertSame($this->data, $this->dot->toArray());
+ }
/**
* Testing the toJson Method.
@@ -644,22 +751,52 @@ public function testToJson()
self::assertSame($this->jsonArray, $decode);
}
-
/**
- * Testing the toArray Method.
+ * Testing the toFlat Method.
*
- * @covers \BinaryCube\DotArray\DotArray::toArray
+ * @covers \BinaryCube\DotArray\DotArray::flatten
+ * @covers \BinaryCube\DotArray\DotArray::pathPattern
+ * @covers \BinaryCube\DotArray\DotArray::wrapSegmentKey
+ * @covers \BinaryCube\DotArray\DotArray::toFlat
* @covers \BinaryCube\DotArray\DotArray::
* @covers \BinaryCube\DotArray\DotArray::
*
* @return void
*/
- public function testToArray()
+ public function testToFlat()
{
- self::assertIsArray($this->dot->toArray());
- self::assertSame($this->data, $this->dot->toArray());
- }
+ $dot = DotArray::create(
+ [
+ 'a' => [
+ 'b' => 'value',
+ ],
+
+ 'b' => [
+ 1,
+ 2,
+ 3,
+ 'array' => [
+ 1,
+ 2,
+ 3,
+ ]
+ ],
+ ]
+ );
+ self::assertSame(
+ [
+ '{a}.{b}' => 'value',
+ '{b}.{0}' => 1,
+ '{b}.{1}' => 2,
+ '{b}.{2}' => 3,
+ '{b}.{array}.{0}' => 1,
+ '{b}.{array}.{1}' => 2,
+ '{b}.{array}.{2}' => 3,
+ ],
+ $dot->toFlat()
+ );
+ }
/**
* Testing the serialize & unserialize Methods.
@@ -687,7 +824,6 @@ public function testSerializable()
self::assertInstanceOf(DotArray::class, $unserialize);
}
-
/**
* Testing the jsonSerialize Methods.
*
@@ -700,7 +836,6 @@ public function testJsonSerialize()
self::assertSame($this->data, $this->dot->jsonSerialize());
}
-
/**
* Testing the getIterator Methods.
*
@@ -715,5 +850,4 @@ public function testIterator()
self::assertSame($this->data, $this->dot->getIterator()->getArrayCopy());
}
-
}
diff --git a/tests/Integration/CreateTest.php b/tests/Unit/CreateTest.php
similarity index 89%
rename from tests/Integration/CreateTest.php
rename to tests/Unit/CreateTest.php
index 4c33e78..7b477e9 100644
--- a/tests/Integration/CreateTest.php
+++ b/tests/Unit/CreateTest.php
@@ -1,9 +1,8 @@
toArray());
+ self::assertIsArray(DotArray::create(1)->toArray());
+ self::assertIsArray(DotArray::create([])->toArray());
+ self::assertIsArray(DotArray::create(DotArray::create([]))->toArray());
+
$dot = new DotArray($this->data);
self::assertIsArray($dot->toArray());
+
self::assertArrayHasKey('empty_array', $dot->toArray());
self::assertArrayHasKey('indexed_array', $dot->toArray());
self::assertArrayHasKey('assoc_array', $dot->toArray());
@@ -96,5 +99,4 @@ public function testCreate()
self::assertArrayHasKey('a', $dot->toArray());
}
-
}
diff --git a/tests/TestCase.php b/tests/Unit/TestCase.php
similarity index 77%
rename from tests/TestCase.php
rename to tests/Unit/TestCase.php
index b776f94..e709d2f 100644
--- a/tests/TestCase.php
+++ b/tests/Unit/TestCase.php
@@ -1,8 +1,6 @@