diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2454616 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +; top-most EditorConfig file +root = true + +# All files. +[*] +end_of_line = LF +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7a6ef08 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/phpcs.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml.dist export-ignore +/tests export-ignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..665dc6c --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. We accept contributions via Pull Requests on [GitHub](https://github.com/dflydev/dflydev-dot-access-data). + +## Pull Requests + +- **[PSR-12 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - The easiest way to apply the conventions is to run `./vendor/bin/phpcbf`. + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Tests must pass** - All automated tests, including things like code style checks, but be passing before we'll consider merging the change. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. + +- **Create feature branches** - Don't ask us to pull from your default branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. + + +## Running Tests + +``` bash +$ composer test +``` + + +**Happy coding**! diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..60c38ea --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,100 @@ +name: Tests + +on: + push: ~ + pull_request: ~ + +jobs: + phpcs: + name: PHPCS + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: 7.1 + extensions: curl, mbstring + coverage: none + tools: composer:v2, cs2pr + + - run: composer update --no-progress + + - run: vendor/bin/phpcs -q --report=checkstyle | cs2pr + + phpunit: + name: PHPUnit on ${{ matrix.php }} ${{ matrix.composer-flags }} + runs-on: ubuntu-latest + strategy: + matrix: + php: ['7.2', '7.3', '7.4'] + coverage: [pcov] + composer-flags: [''] + include: + - php: '7.1' + coverage: xdebug + composer-flags: '' + - php: '8.0' + coverage: false + composer-flags: '--ignore-platform-req=php' + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl + coverage: ${{ matrix.coverage }} + tools: composer:v2 + + - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: "Use PHPUnit 9.3+ on PHP 8" + run: composer require --no-update --dev phpunit/phpunit:^9.3 + if: "matrix.php == '8.0'" + + - run: composer update --no-progress ${{ matrix.composer-flags }} + + - run: vendor/bin/phpunit --no-coverage + if: ${{ matrix.coverage == 'none' }} + + - run: vendor/bin/phpunit --coverage-text + if: ${{ matrix.coverage != 'none' }} + + phpstan: + name: PHPStan + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: 7.1 + extensions: curl + coverage: none + tools: composer:v2 + + - run: composer update --no-progress + + - run: vendor/bin/phpstan analyse --no-progress + + psalm: + name: Psalm + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: 7.1 + extensions: curl + coverage: none + tools: composer:v2 + + - run: composer update --no-progress + + - run: vendor/bin/psalm --no-progress --output-format=github diff --git a/.gitignore b/.gitignore index 987e2a2..a0a4f20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ +.phpcs-cache +.phpunit.result.cache composer.lock +phpcs.xml +phpstan.neon +phpunit.xml +psalm.xml vendor diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7dc0a79..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: php - -php: - - 7.0 - - 7.1 - - 7.2 - -before_script: - - wget -nc http://getcomposer.org/composer.phar - - php composer.phar install - -script: ./vendor/bin/phpunit --coverage-text diff --git a/README.md b/README.md index ebcbbbb..9a73d6f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Given a deep data structure, access data by dot notation. Requirements ------------ - * PHP (7.0+) + * PHP (7.1+) > For PHP (5.3+) please refer to version `1.0`. diff --git a/composer.json b/composer.json index 9911c53..7dcc1a2 100644 --- a/composer.json +++ b/composer.json @@ -23,10 +23,13 @@ } ], "require": { - "php": "^7.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^3.14" }, "autoload": { "psr-4": { @@ -42,5 +45,17 @@ "branch-alias": { "dev-master": "2.0-dev" } + }, + "scripts": { + "phpcs": "phpcs", + "phpstan": "phpstan analyse", + "phpunit": "phpunit --no-coverage", + "psalm": "psalm", + "test": [ + "@phpcs", + "@phpstan", + "@psalm", + "@phpunit" + ] } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..d67debb --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,17 @@ + + + + + + + + + + + + + src + tests + + + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..a1c637a --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,4 @@ +parameters: + level: max + paths: + - src diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ce9bb37..b2c4433 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,14 @@ - + ./tests diff --git a/psalm.xml.dist b/psalm.xml.dist new file mode 100644 index 0000000..729e9a9 --- /dev/null +++ b/psalm.xml.dist @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/src/Data.php b/src/Data.php index 1cec9a6..53ea64a 100644 --- a/src/Data.php +++ b/src/Data.php @@ -18,24 +18,24 @@ class Data implements DataInterface /** * Internal representation of data data * - * @var array + * @var array */ protected $data; /** * Constructor * - * @param array|null $data + * @param array $data */ - public function __construct(array $data = null) + public function __construct(array $data = []) { - $this->data = $data ?: []; + $this->data = $data; } /** * {@inheritdoc} */ - public function append($key, $value = null) + public function append(string $key, $value = null): void { if (0 == strlen($key)) { throw new RuntimeException("Key cannot be an empty string"); @@ -59,9 +59,9 @@ public function append($key, $value = null) } $endKey = array_pop($keyPath); - for ( $i = 0; $i < count($keyPath); $i++ ) { + for ($i = 0; $i < count($keyPath); $i++) { $currentKey =& $keyPath[$i]; - if ( ! isset($currentValue[$currentKey]) ) { + if (! isset($currentValue[$currentKey])) { $currentValue[$currentKey] = []; } $currentValue =& $currentValue[$currentKey]; @@ -81,7 +81,7 @@ public function append($key, $value = null) /** * {@inheritdoc} */ - public function set($key, $value = null) + public function set(string $key, $value = null): void { if (0 == strlen($key)) { throw new RuntimeException("Key cannot be an empty string"); @@ -97,7 +97,7 @@ public function set($key, $value = null) } $endKey = array_pop($keyPath); - for ( $i = 0; $i < count($keyPath); $i++ ) { + for ($i = 0; $i < count($keyPath); $i++) { $currentKey =& $keyPath[$i]; if (!isset($currentValue[$currentKey])) { $currentValue[$currentKey] = []; @@ -113,7 +113,7 @@ public function set($key, $value = null) /** * {@inheritdoc} */ - public function remove($key) + public function remove(string $key): void { if (0 == strlen($key)) { throw new RuntimeException("Key cannot be an empty string"); @@ -129,7 +129,7 @@ public function remove($key) } $endKey = array_pop($keyPath); - for ( $i = 0; $i < count($keyPath); $i++ ) { + for ($i = 0; $i < count($keyPath); $i++) { $currentKey =& $keyPath[$i]; if (!isset($currentValue[$currentKey])) { return; @@ -141,15 +141,17 @@ public function remove($key) /** * {@inheritdoc} + * + * @psalm-mutation-free */ - public function get($key, $default = null) + public function get(string $key, $default = null) { $currentValue = $this->data; $keyPath = explode('.', $key); - for ( $i = 0; $i < count($keyPath); $i++ ) { + for ($i = 0; $i < count($keyPath); $i++) { $currentKey = $keyPath[$i]; - if (!isset($currentValue[$currentKey]) ) { + if (!isset($currentValue[$currentKey])) { return $default; } if (!is_array($currentValue)) { @@ -163,13 +165,15 @@ public function get($key, $default = null) /** * {@inheritdoc} + * + * @psalm-mutation-free */ - public function has($key) + public function has(string $key): bool { $currentValue = &$this->data; $keyPath = explode('.', $key); - for ( $i = 0; $i < count($keyPath); $i++ ) { + for ($i = 0; $i < count($keyPath); $i++) { $currentKey = $keyPath[$i]; if ( !is_array($currentValue) || @@ -185,8 +189,10 @@ public function has($key) /** * {@inheritdoc} + * + * @psalm-mutation-free */ - public function getData($key) + public function getData(string $key): DataInterface { $value = $this->get($key); if (is_array($value) && Util::isAssoc($value)) { @@ -199,7 +205,7 @@ public function getData($key) /** * {@inheritdoc} */ - public function import(array $data, $clobber = true) + public function import(array $data, bool $clobber = true): void { $this->data = Util::mergeAssocArray($this->data, $data, $clobber); } @@ -207,15 +213,17 @@ public function import(array $data, $clobber = true) /** * {@inheritdoc} */ - public function importData(DataInterface $data, $clobber = true) + public function importData(DataInterface $data, bool $clobber = true): void { $this->import($data->export(), $clobber); } /** * {@inheritdoc} + * + * @psalm-mutation-free */ - public function export() + public function export(): array { return $this->data; } diff --git a/src/DataInterface.php b/src/DataInterface.php index 3498f26..fa20196 100644 --- a/src/DataInterface.php +++ b/src/DataInterface.php @@ -19,7 +19,7 @@ interface DataInterface * @param string $key * @param mixed $value */ - public function append($key, $value = null); + public function append(string $key, $value = null): void; /** * Set a value for a key @@ -27,14 +27,14 @@ public function append($key, $value = null); * @param string $key * @param mixed $value */ - public function set($key, $value = null); + public function set(string $key, $value = null): void; /** * Remove a key * * @param string $key */ - public function remove($key); + public function remove(string $key): void; /** * Get the raw value for a key @@ -43,8 +43,10 @@ public function remove($key); * @param mixed $default * * @return mixed + * + * @psalm-mutation-free */ - public function get($key, $default = null); + public function get(string $key, $default = null); /** * Check if the key exists @@ -52,8 +54,10 @@ public function get($key, $default = null); * @param string $key * * @return bool + * + * @psalm-mutation-free */ - public function has($key); + public function has(string $key): bool; /** * Get a data instance for a key @@ -61,16 +65,18 @@ public function has($key); * @param string $key * * @return DataInterface + * + * @psalm-mutation-free */ - public function getData($key); + public function getData(string $key): DataInterface; /** * Import data into existing data * - * @param array $data - * @param bool $clobber + * @param array $data + * @param bool $clobber */ - public function import(array $data, $clobber = true); + public function import(array $data, bool $clobber = true): void; /** * Import data from an external data into existing data @@ -78,12 +84,14 @@ public function import(array $data, $clobber = true); * @param DataInterface $data * @param bool $clobber */ - public function importData(DataInterface $data, $clobber = true); + public function importData(DataInterface $data, bool $clobber = true): void; /** * Export data as raw data * - * @return array + * @return array + * + * @psalm-mutation-free */ - public function export(); + public function export(): array; } diff --git a/src/Util.php b/src/Util.php index 546da22..a9cc2cc 100644 --- a/src/Util.php +++ b/src/Util.php @@ -19,25 +19,31 @@ class Util * Note that this function will return true if an array is empty. Meaning * empty arrays will be treated as if they are associative arrays. * - * @param array $arr + * @param array $arr * - * @return boolean + * @return bool + * + * @psalm-pure */ - public static function isAssoc(array $arr) + public static function isAssoc(array $arr): bool { - return (is_array($arr) && (!count($arr) || count(array_filter(array_keys($arr),'is_string')) == count($arr))); + return !count($arr) || count(array_filter(array_keys($arr), 'is_string')) == count($arr); } /** * Merge contents from one associtative array to another * - * @param array $to - * @param array $from + * @param mixed $to + * @param mixed $from * @param bool $clobber + * + * @return mixed + * + * @psalm-pure */ public static function mergeAssocArray($to, $from, $clobber = true) { - if ( is_array($from) ) { + if (is_array($from)) { foreach ($from as $k => $v) { if (!isset($to[$k])) { $to[$k] = $v; diff --git a/tests/DataTest.php b/tests/DataTest.php index d80e052..b3b1ad1 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -88,7 +88,7 @@ public function testAppend() public function testSet() { - $data = new Data; + $data = new Data(); $this->assertNull($data->get('a')); $this->assertNull($data->get('b.c')); @@ -111,7 +111,7 @@ public function testSet() public function testSetClobberStringInPath() { - $data = new Data; + $data = new Data(); $data->set('a.b.c', 'Should not be able to write to a.b.c.d.e');