diff --git a/.gitattributes b/.gitattributes index ada1786..a3648c5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,6 +15,7 @@ *.bat text *.sql text *.yml text +*.neon text # Ensure those won't be messed up with @@ -25,12 +26,14 @@ # 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 /.scrutinizer.yml export-ignore 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/.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 ecca587..3621a36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,3 +20,5 @@ script: after_script: - 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 48e307a..6993dcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,24 @@ 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 ----------------------------- diff --git a/README.md b/README.md index 28345dc..fa542b3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# DotArray - Sail through array using the dot notation +# PHP Dot-Array :: Sail through array using the dot notation

~ Enjoy your :coffee: ~

@@ -8,14 +8,13 @@ Accessing PHP Arrays via DOT notation is easy as: ```php DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{some.dotted.key}') ``` -[![Minimum PHP Version `PHP >= 7.1`](https://img.shields.io/badge/php-%3E%3D%207.1-8892BF.svg)][php-site] -[![Latest Stable Version](https://poser.pugx.org/binary-cube/dot-array/version)][packagist] -[![Total Downloads](https://poser.pugx.org/binary-cube/dot-array/downloads)][packagist] -[![Build Status](https://travis-ci.org/binary-cube/dot-array.svg?branch=master)](https://travis-ci.org/binary-cube/dot-array) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/003187f2016e4c4cb1b014ccc9bdb5c0)](https://www.codacy.com/app/microThread/dot-array) -[![Code Coverage](https://scrutinizer-ci.com/g/binary-cube/dot-array/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/binary-cube/dot-array/?branch=master) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/binary-cube/dot-array/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/binary-cube/dot-array/?branch=master) -[![License](https://poser.pugx.org/binary-cube/dot-array/license)][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] ----- @@ -103,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); @@ -111,14 +112,31 @@ 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. $dot['books.{sci-fi & fantasy}'] = []; ``` +- **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.{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']]); @@ -137,31 +155,22 @@ DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{som ); ``` -- **delete** *(unset(...))*: - - ```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']); - ``` - - **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'; }); @@ -186,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**: @@ -218,24 +227,24 @@ 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**: - - ```php - // Getting the internal raw array. + > Getting the internal raw array. + - ```php // Example 1. $dot->toArray(); @@ -244,9 +253,9 @@ DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{som ``` - **toJson**: - - ```php - // Getting the internal raw array as JSON. + > Getting the internal raw array as JSON. + - ```php // Example 1. $dot->toJson(); @@ -255,17 +264,25 @@ DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{som ``` - **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, + ] ], ] ); @@ -274,12 +291,14 @@ DotArray::create(['config' => ['some.dotted.key' => 'value']])->get('config.{som /* The output will be an array: - [ - '{{a}}.{{b}}' => 'value', - '{{b}}.{{0}}' => 1, - '{{b}}.{{1}}' => 2, - '{{b}}.{{2}}' => 3, + '{a}.{b}' => 'value', + '{b}.{0}' => 1, + '{b}.{1}' => 2, + '{b}.{2}' => 3, + '{b}.{array}.{0}' => 1, + '{b}.{array}.{1}' => 2, + '{b}.{array}.{2}' => 3, ], */ ``` @@ -318,7 +337,7 @@ $dummyArray = [ ], ], - 'childre\'s books' => + 'children\'s books' => [ [ 'name' => 'Harry Potter and the Order of the Phoenix', @@ -358,7 +377,7 @@ $dummyArray = [ 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][new-issue]. +If your problem or idea is not addressed yet, [please open a new issue][link-new-issue]. @@ -372,14 +391,14 @@ 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][code-of-conduct]. +[Read the full Code of Conduct][link-code-of-conduct]. #### Versioning -Through the development of new versions, we're going use the [Semantic Versioning][semver]. +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 @@ -393,25 +412,38 @@ Example: `1.0.0`. * **Banciu N. Cristian Mihai** -See also the list of [contributors][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. + -[domain]: https://binary-cube.com -[homepage]: https://binary-cube.com -[git-source]: https://github.com/binary-cube/dot-array -[php-site]: https://php.net -[semver]: https://semver.org -[code-of-conduct]: https://github.com/binary-cube/dot-array/blob/master/code-of-conduct.md -[license]: https://github.com/binary-cube/dot-array/blob/master/LICENSE -[contributors]: https://github.com/binary-cube/dot-array/graphs/contributors -[new-issue]: https://github.com/binary-cube/dot-array/issues/new -[packagist]: https://packagist.org/packages/binary-cube/dot-array +[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 4ccf1fc..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": { @@ -69,12 +70,14 @@ "scripts": { "check": [ "@cs-check", + "@phpstan", "@tests" ], "generate-reports": [ "@create-folders", "@cs-report", + "@phpstan-report", "@phpmd-report", "@tests-report-html", "@tests-report-xml", @@ -87,14 +90,16 @@ "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;", - "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;" + "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 f03c800..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/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 8b08e86..321e6af 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,8 +14,8 @@ processIsolation="false"> - - ./tests + + ./tests/Unit diff --git a/src/DotArray.php b/src/DotArray.php index bef16f1..2ee36fb 100644 --- a/src/DotArray.php +++ b/src/DotArray.php @@ -19,14 +19,12 @@ class DotArray implements { /* Traits. */ - use DotPathTrait; + use DotFilteringTrait; - /** - * Unique object identifier. - * - * @var string - */ - protected $uniqueIdentifier; + private const TEMPLATE_PATTERN = '#(?|(?|[%s](.*?)[%s])|(.*?))(?:$|\.+)#i'; + private const WRAP_KEY = '{%s}'; + private const TOKEN_START = ['\'', '\"', '\[', '\(', '\{']; + private const TOKEN_END = ['\'', '\"', '\]', '\)', '\}']; /** * Stores the original data. @@ -35,7 +33,6 @@ class DotArray implements */ protected $items; - /** * Creates an DotArray object. * @@ -48,7 +45,6 @@ public static function create($items) return (new static($items)); } - /** * @param string $json * @@ -59,6 +55,130 @@ public static function createFromJson($json) return static::create(\json_decode($json, true)); } + /** + * Getting the path pattern. + * + * 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} + * + * @return string + */ + protected static function pathPattern() + { + return ( + vsprintf( + self::TEMPLATE_PATTERN, + [ + \implode('', self::TOKEN_START), + \implode('', self::TOKEN_END), + ] + ) + ); + } + + /** + * Converts dot string path to segments. + * + * @param string $path + * + * @return array + */ + final protected static function pathToSegments($path) + { + $path = \trim($path, " \t\n\r\0\x0B\."); + $segments = []; + $matches = []; + + if (\mb_strlen($path, 'UTF-8') === 0) { + return []; + } + + \preg_match_all(self::pathPattern(), $path, $matches); + + if (!empty($matches[1])) { + $matches = $matches[1]; + + $segments = \array_filter( + $matches, + function ($match) { + return (\mb_strlen($match, 'UTF-8') > 0); + } + ); + } + + unset($path, $matches); + + return (empty($segments) ? [] : $segments); + } + + /** + * Wrap a given string into special characters. + * + * @param string $key + * + * @return string + */ + final protected static function wrapSegmentKey($key) + { + return \vsprintf(self::WRAP_KEY, [$key]); + } + + /** + * @param array $segments + * + * @return string + */ + final protected static function segmentsToKey(array $segments) + { + return ( + \implode( + '.', + \array_map( + 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 @@ -67,7 +187,7 @@ public static function createFromJson($json) * * @return array */ - protected static function normalize($items) + final protected static function normalize($items) { if ($items instanceof self) { $items = $items->toArray(); @@ -76,25 +196,24 @@ protected static function normalize($items) if (\is_array($items)) { foreach ($items as $k => $v) { if (\is_array($v) || $v instanceof self) { - $v = static::normalize($v); + $v = self::normalize($v); } $items[$k] = $v; } } - return (array) (empty($items) ? [] : $items); + return (array) $items; } - /** * @param array|DotArray|mixed $array1 * @param null|array|DotArray|mixed $array2 * * @return array */ - protected static function mergeRecursive($array1, $array2 = null) + final protected static function mergeRecursive($array1, $array2 = null) { - $args = static::normalize(\func_get_args()); + $args = self::normalize(\func_get_args()); $res = \array_shift($args); while (!empty($args)) { @@ -105,7 +224,7 @@ protected static function mergeRecursive($array1, $array2 = null) } if (\is_array($v) && isset($res[$k]) && \is_array($res[$k])) { - $v = static::mergeRecursive($res[$k], $v); + $v = self::mergeRecursive($res[$k], $v); } $res[$k] = $v; @@ -115,102 +234,6 @@ protected static function mergeRecursive($array1, $array2 = null) return $res; } - - /** - * 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. * @@ -218,21 +241,17 @@ protected static function operators() */ public function __construct($items = []) { - $this->items = static::normalize($items); + $this->items = self::normalize($items); } - /** * DotArray Destructor. */ public function __destruct() { - unset($this->uniqueIdentifier); unset($this->items); - unset($this->nestedPathPattern); } - /** * Call object as function. * @@ -245,27 +264,6 @@ public function __invoke($key = null) return $this->get($key); } - - /** - * @return string - */ - public function uniqueIdentifier() - { - if (empty($this->uniqueIdentifier)) { - $this->uniqueIdentifier = \vsprintf( - '{%s}.{%s}.{%s}', - [ - static::class, - \uniqid('', true), - \microtime(true), - ] - ); - } - - return $this->uniqueIdentifier; - } - - /** * Merges one or more arrays into master recursively. * If each array has an element with the same string key value, the latter @@ -286,32 +284,25 @@ 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 = static::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; } @@ -323,7 +314,6 @@ protected function &read($key, $default) return $items; } - /** * @param string $key * @param mixed $value @@ -332,7 +322,7 @@ protected function &read($key, $default) */ protected function write($key, $value) { - $segments = static::pathToSegments($key); + $segments = self::pathToSegments($key); $count = \count($segments); $items = &$this->items; @@ -340,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] = []; @@ -352,15 +339,16 @@ protected function write($key, $value) $items = &$items[$segment]; } - unset($segments, $count); - if (\is_array($value) || $value instanceof self) { - $value = static::normalize($value); + $value = self::normalize($value); } $items = $value; - } + if (!\is_array($this->items)) { + $this->items = self::normalize($this->items); + } + } /** * Delete the given key or keys. @@ -371,19 +359,17 @@ protected function write($key, $value) */ protected function remove($key) { - $segments = static::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; @@ -391,11 +377,8 @@ protected function remove($key) $items = &$items[$segment]; } - - unset($segments, $count); } - /** * @param string $key * @@ -403,12 +386,11 @@ protected function remove($key) */ public function has($key) { - $identifier = $this->uniqueIdentifier(); + $identifier = \uniqid(static::class, true); return ($identifier !== $this->read($key, $identifier)); } - /** * Check if a given key contains empty values (null, [], 0, false) * @@ -418,12 +400,9 @@ public function has($key) */ public function isEmpty($key = null) { - $items = $this->read($key, null); - - return empty($items); + return empty($this->read($key, null)); } - /** * @param null|string $key * @param null|mixed $default @@ -441,16 +420,15 @@ 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) (!isset($keys) ? [$keys] : $keys); @@ -461,7 +439,6 @@ public function set($keys, $value) return $this; } - /** * Delete the given key or keys. * @@ -480,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 */ @@ -500,182 +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 * @@ -693,7 +504,6 @@ public function offsetExists($offset) return $this->has($offset); } - /** * Offset to retrieve * @@ -710,7 +520,6 @@ public function &offsetGet($offset) return $this->read($offset, null); } - /** * Offset to set * @@ -728,7 +537,6 @@ public function offsetSet($offset, $value) $this->write($offset, $value); } - /** * Offset to unset * @@ -750,7 +558,6 @@ public function offsetUnset($offset) $this->remove($offset); } - /** * Count elements of an object * @@ -767,39 +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); - } - - - /** - * Flatten the internal array using the dot delimiter, - * also the keys are wrapped inside {{key}} (2 x curly braces). - * - * @return array - */ - public function toFlat() - { - return static::flatten($this->items); - } - - /** * Specify data which should be serialized to JSON * @@ -815,7 +589,6 @@ public function jsonSerialize() return $this->items; } - /** * String representation of object * @@ -830,7 +603,6 @@ public function serialize() return \serialize($this->items); } - /** * Constructs the object * @@ -847,7 +619,6 @@ public function unserialize($serialized) $this->items = \unserialize($serialized); } - /** * Retrieve an external iterator. * @@ -862,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/src/DotPathTrait.php b/src/DotPathTrait.php deleted file mode 100644 index dd003b5..0000000 --- a/src/DotPathTrait.php +++ /dev/null @@ -1,175 +0,0 @@ - - * @license https://github.com/binary-cube/dot-array/blob/master/LICENSE - * @link https://github.com/binary-cube/dot-array - */ -trait DotPathTrait -{ - - /** - * Internal Dot Path Config. - * - * @var array - */ - protected static $dotPathConfig = [ - 'template' => '#(?|(?|[](.*?)[])|(.*?))(?:$|\.+)#i', - 'wrapKey' => '{{%s}}', - 'wildcards' => [ - '' => ['\'', '\"', '\[', '\(', '\{'], - '' => ['\'', '\"', '\]', '\)', '\}'], - ], - ]; - - /** - * The cached 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 static $dotPathPattern; - - - /** - * Getting the dot path pattern. - * - * @return string - */ - protected static function dotPathPattern() - { - if (empty(self::$dotPathPattern)) { - $path = self::$dotPathConfig['template']; - - foreach (self::$dotPathConfig['wildcards'] as $wildcard => $tokens) { - $path = \str_replace($wildcard, \implode('', $tokens), $path); - } - - self::$dotPathPattern = $path; - } - - return self::$dotPathPattern; - } - - - /** - * Converts dot string path to segments. - * - * @param string $path - * - * @return array - */ - protected static function pathToSegments($path) - { - $path = \trim($path, " \t\n\r\0\x0B\."); - $segments = []; - $matches = []; - - if (\mb_strlen($path, 'UTF-8') === 0) { - return []; - } - - \preg_match_all(static::dotPathPattern(), $path, $matches); - - if (!empty($matches[1])) { - $matches = $matches[1]; - - $segments = \array_filter( - $matches, - function ($match) { - return (\mb_strlen($match, 'UTF-8') > 0); - } - ); - } - - unset($path, $matches); - - return $segments; - } - - - /** - * Wrap a given string into special characters. - * - * @param string $key - * - * @return string - */ - protected static function wrapSegmentKey($key) - { - return vsprintf(static::$dotPathConfig['wrapKey'], [$key]); - } - - - /** - * @param array $segments - * - * @return string - */ - protected static function segmentsToKey(array $segments) - { - return ( - \implode( - '', - \array_map( - [static::class, 'wrapSegmentKey'], - $segments - ) - ) - ); - } - - - /** - * Flatten the internal array using the dot delimiter, - * also the keys are wrapped inside {{key}} (2 x curly braces). - * - * @param array $items - * @param string $prepend - * - * @return array - */ - protected static function flatten(array $items, $prepend = '') - { - $flatten = []; - - foreach ($items as $key => $value) { - $wrapKey = static::wrapSegmentKey($key); - - if (\is_array($value) && !empty($value)) { - $flatten = array_merge( - $flatten, - static::flatten( - $value, - ($prepend . $wrapKey . '.') - ) - ); - - continue; - } - - $flatten[$prepend . $wrapKey] = $value; - } - - return $flatten; - } - - -} 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. * - * @covers \BinaryCube\DotArray\DotPathTrait * @covers \BinaryCube\DotArray\DotArray::get * @covers \BinaryCube\DotArray\DotArray::toArray * @covers \BinaryCube\DotArray\DotArray:: @@ -104,11 +168,9 @@ public function testGet() self::assertIsString($this->dot['mixed_array.hello-world.{Nǐ hǎo}']); } - /** * Testing the Set Method. * - * @covers \BinaryCube\DotArray\DotPathTrait * @covers \BinaryCube\DotArray\DotArray::set * @covers \BinaryCube\DotArray\DotArray::toArray * @covers \BinaryCube\DotArray\DotArray:: @@ -158,13 +220,13 @@ 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. * - * @covers \BinaryCube\DotArray\DotPathTrait * @covers \BinaryCube\DotArray\DotArray::has * @covers \BinaryCube\DotArray\DotArray:: * @covers \BinaryCube\DotArray\DotArray:: @@ -191,11 +253,9 @@ public function testHas() self::assertNotTrue($this->dot->has('a.b.c.d')); } - /** * Testing the isEmpty Method. * - * @covers \BinaryCube\DotArray\DotPathTrait * @covers \BinaryCube\DotArray\DotArray::isEmpty * @covers \BinaryCube\DotArray\DotArray:: * @covers \BinaryCube\DotArray\DotArray:: @@ -222,11 +282,9 @@ public function testIsEmpty() self::assertIsBool($this->dot->get('dotObject')->isEmpty()); } - /** * Testing the Delete Method. * - * @covers \BinaryCube\DotArray\DotPathTrait * @covers \BinaryCube\DotArray\DotArray::delete * @covers \BinaryCube\DotArray\DotArray:: * @covers \BinaryCube\DotArray\DotArray:: @@ -258,11 +316,9 @@ public function testDelete() self::assertTrue(array_key_exists('one', $this->dot['assoc_array'])); } - /** * Testing the Clear Method. * - * @covers \BinaryCube\DotArray\DotPathTrait * @covers \BinaryCube\DotArray\DotArray::clear * @covers \BinaryCube\DotArray\DotArray:: * @covers \BinaryCube\DotArray\DotArray:: @@ -287,11 +343,9 @@ public function testClear() self::assertEmpty($users->toArray()); } - /** * Testing the Merge Method. * - * @covers \BinaryCube\DotArray\DotPathTrait * @covers \BinaryCube\DotArray\DotArray::merge * @covers \BinaryCube\DotArray\DotArray:: * @covers \BinaryCube\DotArray\DotArray:: @@ -380,11 +434,9 @@ public function testMerge() self::assertCount(3, $this->dot->get('mixed_array.{👋.🤘.some-key}.config.memcached.servers')); } - /** * Testing the Count Method. * - * @covers \BinaryCube\DotArray\DotPathTrait * @covers \BinaryCube\DotArray\DotArray::count * @covers \BinaryCube\DotArray\DotArray:: * @covers \BinaryCube\DotArray\DotArray:: @@ -402,11 +454,10 @@ public function testCount() self::assertEquals(1, count($this->dot['assoc_array']['three'])); } - /** * Testing the Find Method. * - * @covers \BinaryCube\DotArray\DotPathTrait + * @covers \BinaryCube\DotArray\DotFilteringTrait * @covers \BinaryCube\DotArray\DotArray::find * @covers \BinaryCube\DotArray\DotArray:: * @covers \BinaryCube\DotArray\DotArray:: @@ -442,11 +493,10 @@ function () { ); } - /** * Testing the Filter Method. * - * @covers \BinaryCube\DotArray\DotPathTrait + * @covers \BinaryCube\DotArray\DotFilteringTrait * @covers \BinaryCube\DotArray\DotArray::filter * @covers \BinaryCube\DotArray\DotArray:: * @covers \BinaryCube\DotArray\DotArray:: @@ -467,11 +517,10 @@ function ($value) { self::assertSame([1, 2, 3, 4], $under->toArray()); } - /** * Testing the FilterBy Method. * - * @covers \BinaryCube\DotArray\DotPathTrait + * @covers \BinaryCube\DotArray\DotFilteringTrait * @covers \BinaryCube\DotArray\DotArray::filterBy * @covers \BinaryCube\DotArray\DotArray:: * @covers \BinaryCube\DotArray\DotArray:: @@ -578,11 +627,10 @@ function ($value) { ); } - /** * Testing the Where Method. * - * @covers \BinaryCube\DotArray\DotPathTrait + * @covers \BinaryCube\DotArray\DotFilteringTrait * @covers \BinaryCube\DotArray\DotArray::where * @covers \BinaryCube\DotArray\DotArray:: * @covers \BinaryCube\DotArray\DotArray:: @@ -652,11 +700,9 @@ function ($value) { self::assertSame($user1, $users->toArray()); } - /** * Testing the First Method. * - * @covers \BinaryCube\DotArray\DotPathTrait * @covers \BinaryCube\DotArray\DotArray::first * @covers \BinaryCube\DotArray\DotArray:: * @covers \BinaryCube\DotArray\DotArray:: @@ -671,7 +717,6 @@ public function testFirst() ); } - /** * Testing the toArray Method. * @@ -687,7 +732,6 @@ public function testToArray() self::assertSame($this->data, $this->dot->toArray()); } - /** * Testing the toJson Method. * @@ -707,21 +751,19 @@ public function testToJson() self::assertSame($this->jsonArray, $decode); } - /** - * Testing the toFlatten Method. + * Testing the toFlat Method. * - * @covers \BinaryCube\DotArray\DotPathTrait - * @covers \BinaryCube\DotArray\DotPathTrait::flatten - * @covers \BinaryCube\DotArray\DotPathTrait::dotPathPattern - * @covers \BinaryCube\DotArray\DotPathTrait::wrapSegmentKey + * @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 testToFlatten() + public function testToFlat() { $dot = DotArray::create( [ @@ -733,22 +775,29 @@ public function testToFlatten() 1, 2, 3, + 'array' => [ + 1, + 2, + 3, + ] ], ] ); self::assertSame( [ - '{{a}}.{{b}}' => 'value', - '{{b}}.{{0}}' => 1, - '{{b}}.{{1}}' => 2, - '{{b}}.{{2}}' => 3, + '{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. * @@ -775,7 +824,6 @@ public function testSerializable() self::assertInstanceOf(DotArray::class, $unserialize); } - /** * Testing the jsonSerialize Methods. * @@ -788,7 +836,6 @@ public function testJsonSerialize() self::assertSame($this->data, $this->dot->jsonSerialize()); } - /** * Testing the getIterator Methods. * @@ -803,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 88% rename from tests/TestCase.php rename to tests/Unit/TestCase.php index a2262e8..e709d2f 100644 --- a/tests/TestCase.php +++ b/tests/Unit/TestCase.php @@ -1,6 +1,6 @@