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 -[![Require `PHP >= 7.1`](https://img.shields.io/badge/Require%20PHP-%3E%3D%207.1-brightgreen.svg)](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}') ``` -[![Latest Stable Version](https://poser.pugx.org/binary-cube/dot-array/version)](https://packagist.org/packages/binary-cube/dot-array) -[![Total Downloads](https://poser.pugx.org/binary-cube/dot-array/downloads)](https://packagist.org/packages/binary-cube/dot-array) -[![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] ----- + + + ## 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 @@