From 699d41d0e58017fe080343ed1b037fbf77fecd40 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Fri, 1 May 2020 10:10:58 +0200 Subject: [PATCH 01/32] Drop deprecated functions `withMap()` and `withFilter()` --- src/IterableObject.php | 20 -------------------- tests/TestIterableObject.php | 32 -------------------------------- 2 files changed, 52 deletions(-) diff --git a/src/IterableObject.php b/src/IterableObject.php index c0caeea..b8eec16 100644 --- a/src/IterableObject.php +++ b/src/IterableObject.php @@ -79,26 +79,6 @@ public function map($map) return new self($this->iterable, $this->filter, $map); } - /** - * @param callable $filter - * @return self - * @deprecated Use IterableObject::filter instead. - */ - public function withFilter($filter) - { - return $this->filter($filter); - } - - /** - * @param callable $map - * @return self - * @deprecated Use IterableObject::map instead. - */ - public function withMap($map) - { - return $this->map($map); - } - /** * @inheritdoc */ diff --git a/tests/TestIterableObject.php b/tests/TestIterableObject.php index 108a4ee..ea43d00 100644 --- a/tests/TestIterableObject.php +++ b/tests/TestIterableObject.php @@ -43,38 +43,6 @@ public function testFromTraversableToArray($data, $filter = null, $map = null, $ $this->assertEquals($expectedResult, $iterableObject->asArray()); } - public function testFilterMutator() - { - $filter = function ($value) { - return 'bar' === $value; - }; - $iterableObject = iterable(array('foo', 'bar'))->withFilter($filter); - $this->assertEquals(array(1 => 'bar'), iterator_to_array($iterableObject)); - } - - public function testMapMutator() - { - $map = 'strtoupper'; - $iterableObject = iterable(array('foo', 'bar'))->withMap($map); - $this->assertEquals(array('FOO', 'BAR'), iterator_to_array($iterableObject)); - } - - public function testFilterAndMapMutators() - { - $filter = function ($value) { - return 'bar' === $value; - }; - $map = 'strtoupper'; - $iterableObject = iterable(array('foo', 'bar'))->withMap($map)->withFilter($filter); - $this->assertEquals(array(1 => 'BAR'), iterator_to_array($iterableObject)); - $iterableObject = iterable(array('foo', 'bar'))->map($map)->filter($filter); - $this->assertEquals(array(1 => 'BAR'), iterator_to_array($iterableObject)); - $iterableObject = iterable(array('foo', 'bar'))->withFilter($filter)->withMap($map); - $this->assertEquals(array(1 => 'BAR'), iterator_to_array($iterableObject)); - $iterableObject = iterable(array('foo', 'bar'))->filter($filter)->map($map); - $this->assertEquals(array(1 => 'BAR'), iterator_to_array($iterableObject)); - } - public function dataProvider() { $data = array('foo', 'bar'); From 956c75a7158bf31cbad72b2204e8597833c5108e Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Fri, 1 May 2020 10:23:37 +0200 Subject: [PATCH 02/32] Use PHPUnit naming convention *Test --- phpunit.xml.dist | 7 +------ tests/{TestIsIterable.php => IsIterableTest.php} | 2 +- tests/{TestIterableFilter.php => IterableFilterTest.php} | 6 ++++-- tests/{TestIterableMap.php => IterableMapTest.php} | 6 ++++-- tests/{TestIterableObject.php => IterableObjectTest.php} | 4 +++- tests/{TestIterableReduce.php => IterableReduceTest.php} | 2 +- tests/{TestIterableToArray.php => IterableToArrayTest.php} | 2 +- ...ableToTraversable.php => IterableToTraversableTest.php} | 2 +- 8 files changed, 16 insertions(+), 15 deletions(-) rename tests/{TestIsIterable.php => IsIterableTest.php} (95%) rename tests/{TestIterableFilter.php => IterableFilterTest.php} (90%) rename tests/{TestIterableMap.php => IterableMapTest.php} (89%) rename tests/{TestIterableObject.php => IterableObjectTest.php} (96%) rename tests/{TestIterableReduce.php => IterableReduceTest.php} (92%) rename tests/{TestIterableToArray.php => IterableToArrayTest.php} (97%) rename tests/{TestIterableToTraversable.php => IterableToTraversableTest.php} (94%) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9ca55d7..ed2e5d0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -17,12 +17,7 @@ > - tests/TestIsIterable.php - tests/TestIterableToArray.php - tests/TestIterableToTraversable.php - tests/TestIterableFilter.php - tests/TestIterableMap.php - tests/TestIterableObject.php + tests diff --git a/tests/TestIsIterable.php b/tests/IsIterableTest.php similarity index 95% rename from tests/TestIsIterable.php rename to tests/IsIterableTest.php index 8426ed8..7449010 100644 --- a/tests/TestIsIterable.php +++ b/tests/IsIterableTest.php @@ -2,7 +2,7 @@ use PHPUnit\Framework\TestCase; -class TestIsIterable extends TestCase +final class IsIterableTest extends TestCase { public function testFunctionExists() diff --git a/tests/TestIterableFilter.php b/tests/IterableFilterTest.php similarity index 90% rename from tests/TestIterableFilter.php rename to tests/IterableFilterTest.php index cd9d22e..e222545 100644 --- a/tests/TestIterableFilter.php +++ b/tests/IterableFilterTest.php @@ -1,6 +1,8 @@ Date: Fri, 15 Jan 2021 22:39:46 +0100 Subject: [PATCH 03/32] Drop EOLed php versions - add Github Actions to replace old CI (part 1/3) - upgrade PHPUnit to v9 - lock symfony/var-dumper to v5.2 as @stable is too wide for testing (lowest is incompatible, symfony/var-dumper/Resources/functions/dump.php is missing) --- .github/workflows/continuous-integration.yml | 73 ++++++++++++++++++++ .scrutinizer.yml | 32 --------- .travis.yml | 37 ---------- README.md | 11 +-- composer.json | 6 +- phpunit.xml.dist | 33 +++------ src/iterable-functions.php | 37 ++++++++-- src/iterable-map-php53.php | 35 ---------- src/iterable-map-php55.php | 34 --------- tests/IterableFilterTest.php | 5 +- tests/IterableMapTest.php | 5 +- tests/IterableToTraversableTest.php | 6 +- tests/bootstrap.php | 7 ++ 13 files changed, 140 insertions(+), 181 deletions(-) create mode 100644 .github/workflows/continuous-integration.yml delete mode 100644 .scrutinizer.yml delete mode 100644 .travis.yml delete mode 100644 src/iterable-map-php53.php delete mode 100644 src/iterable-map-php55.php create mode 100644 tests/bootstrap.php diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..799014e --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,73 @@ +name: "Continuous Integration" + +on: + pull_request: + push: + schedule: + - cron: "0 0 1 * *" + +jobs: + phpunit: + name: "PHPUnit" + runs-on: "ubuntu-20.04" + + strategy: + matrix: + php-version: + - "7.3" + - "7.4" + - "8.0" + dependencies: + - "highest" + include: + - dependencies: "lowest" + php-version: "7.3" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + with: + fetch-depth: 2 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php-version }}" + coverage: "pcov" + ini-values: "zend.assertions=1" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v1" + with: + dependency-versions: "${{ matrix.dependencies }}" + + - name: "Run PHPUnit" + run: "vendor/bin/phpunit --coverage-clover=coverage.xml" + + - name: "Upload coverage file" + uses: "actions/upload-artifact@v2" + with: + name: "phpunit-${{ matrix.deps }}-${{ matrix.php-version }}.coverage" + path: "coverage.xml" + + upload_coverage: + name: "Upload coverage to Codecov" + runs-on: "ubuntu-20.04" + needs: + - "phpunit" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + with: + fetch-depth: 2 + + - name: "Download coverage files" + uses: "actions/download-artifact@v2" + with: + path: "reports" + + - name: "Upload to Codecov" + uses: "codecov/codecov-action@v1" + with: + directory: reports diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 5f9e672..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,32 +0,0 @@ -filter: - excluded_paths: [tests/*] -checks: - php: - code_rating: true - remove_extra_empty_lines: true - remove_php_closing_tag: true - remove_trailing_whitespace: true - fix_use_statements: - remove_unused: true - preserve_multiple: false - preserve_blanklines: true - order_alphabetically: true - fix_php_opening_tag: true - fix_linefeed: true - fix_line_ending: true - fix_identation_4spaces: true - fix_doc_comments: true -tools: - php_analyzer: true - php_code_coverage: false - php_code_sniffer: - config: - standard: PSR2 - filter: - paths: ['src'] - php_loc: - enabled: true - excluded_dirs: [vendor, tests] - php_cpd: - enabled: true - excluded_dirs: [vendor, tests] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d2408be..0000000 --- a/.travis.yml +++ /dev/null @@ -1,37 +0,0 @@ -language: php - -php: - - '5.6' - - '7.0' - - '7.1' - - '7.2' - - '7.3' - - '7.4' - - 'nightly' - -matrix: - include: - - php: '5.3' - dist: precise - - php: '5.4' - dist: trusty - - php: '5.5' - dist: trusty - allow_failures: - - php: 'nightly' - -before_script: - - travis_retry composer self-update - - travis_retry composer global require hirak/prestissimo - - travis_retry composer install --no-interaction --prefer-dist - - travis_retry phpenv rehash - -script: - - if [[ ${TRAVIS_PHP_VERSION:0:3} == "5.3" ]]; then composer require --dev symfony/polyfill-php54; fi - - if [[ $TRAVIS_PHP_VERSION = 5.5.* || $TRAVIS_PHP_VERSION = 5.6.* || $TRAVIS_PHP_VERSION = 7.* ]]; then composer require --dev php-coveralls/php-coveralls; fi - - if [[ $TRAVIS_PHP_VERSION = 5.5.* || $TRAVIS_PHP_VERSION = 5.6.* || $TRAVIS_PHP_VERSION = 7.* ]]; then ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml; else ./vendor/bin/phpunit; fi - - ./vendor/bin/phpcs --standard=psr2 -n src/ - - mkdir -p build/logs - -after_script: - - php vendor/bin/php-coveralls -v diff --git a/README.md b/README.md index 1bb43b4..74aedf6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ [![Latest Stable Version](https://poser.pugx.org/bentools/iterable-functions/v/stable)](https://packagist.org/packages/bentools/iterable-functions) -[![License](https://poser.pugx.org/bentools/iterable-functions/license)](https://packagist.org/packages/bentools/iterable-functions) -[![Build Status](https://img.shields.io/travis/bpolaszek/php-iterable-functions/master.svg?style=flat-square)](https://travis-ci.org/bpolaszek/php-iterable-functions) -[![Coverage Status](https://coveralls.io/repos/github/bpolaszek/php-iterable-functions/badge.svg?branch=master)](https://coveralls.io/github/bpolaszek/php-iterable-functions?branch=master) -[![Quality Score](https://img.shields.io/scrutinizer/g/bpolaszek/php-iterable-functions.svg?style=flat-square)](https://scrutinizer-ci.com/g/bpolaszek/php-iterable-functions) +[![GitHub Actions][GA master image]][GA master] +[![Code Coverage][Coverage image]][CodeCov Master] [![Total Downloads](https://poser.pugx.org/bentools/iterable-functions/downloads)](https://packagist.org/packages/bentools/iterable-functions) Iterable functions @@ -191,3 +189,8 @@ Unit tests ``` ./vendor/bin/phpunit ``` + +[CodeCov Master]: https://codecov.io/gh/bpolaszek/php-iterable-functions/branch/2.0.x-dev +[Coverage image]: https://codecov.io/gh/bpolaszek/php-iterable-functions/branch/2.0.x-dev/graph/badge.svg +[GA master]: https://github.com/bpolaszek/php-iterable-functions/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A2.0.x-dev +[GA master image]: https://github.com/bpolaszek/php-iterable-functions/workflows/Continuous%20Integration/badge.svg diff --git a/composer.json b/composer.json index 31041a7..cca90ba 100644 --- a/composer.json +++ b/composer.json @@ -15,11 +15,11 @@ ] }, "require": { - "php": ">=5.3" + "php": "^7.3 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.0|^5.0|^6.0|^7.0", + "phpunit/phpunit": "^9", "squizlabs/php_codesniffer": "^2.0|^3.4", - "symfony/var-dumper": "@stable" + "symfony/var-dumper": "^5.2" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ed2e5d0..9247927 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,32 +1,21 @@ - tests - - - ./src - - src/iterable-map-php53.php - src/iterable-map-php55.php - - - + + + src + + diff --git a/src/iterable-functions.php b/src/iterable-functions.php index de15eeb..6fbdc9e 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -2,12 +2,41 @@ use BenTools\IterableFunctions\IterableObject; -if (version_compare(PHP_VERSION, '5.5') >= 0) { - include_once __DIR__ . '/iterable-map-php55.php'; -} else { - include_once __DIR__ . '/iterable-map-php53.php'; +if (!function_exists('iterable_map')) { + + /** + * Maps a callable to an iterable. + * + * @param iterable|array|\Traversable $iterable + * @param callable $map + * @return array|ArrayIterator + * @throws InvalidArgumentException + */ + function iterable_map($iterable, $map) + { + if (!is_iterable($iterable)) { + throw new \InvalidArgumentException( + sprintf('Expected array or Traversable, got %s', is_object($iterable) ? get_class($iterable) : gettype($iterable)) + ); + } + + // Cannot rely on callable type-hint on PHP 5.3 + if (null !== $map && !is_callable($map) && !$map instanceof Closure) { + throw new InvalidArgumentException( + sprintf('Expected callable, got %s', is_object($map) ? get_class($map) : gettype($map)) + ); + } + + if ($iterable instanceof Traversable) { + return new ArrayIterator(array_map($map, iterator_to_array($iterable))); + } + + return array_map($map, $iterable); + } + } + if (!function_exists('is_iterable')) { /** diff --git a/src/iterable-map-php53.php b/src/iterable-map-php53.php deleted file mode 100644 index 3918de9..0000000 --- a/src/iterable-map-php53.php +++ /dev/null @@ -1,35 +0,0 @@ - $value) { - yield $key => $map($value); - } - }; - - return $generator($iterable, $map); - } - - return array_map($map, $iterable); - } - -} diff --git a/tests/IterableFilterTest.php b/tests/IterableFilterTest.php index e222545..fdc2539 100644 --- a/tests/IterableFilterTest.php +++ b/tests/IterableFilterTest.php @@ -23,11 +23,10 @@ public function testTraversableFilter() $this->assertEquals(array(1 => 'bar'), iterable_to_array(iterable_filter($iterable, $filter))); } - /** - * @expectedException InvalidArgumentException - */ public function testInvalidIterable() { + $this->expectException(InvalidArgumentException::class); + $filter = function () { return true; }; diff --git a/tests/IterableMapTest.php b/tests/IterableMapTest.php index 260e7c9..0746827 100644 --- a/tests/IterableMapTest.php +++ b/tests/IterableMapTest.php @@ -19,11 +19,10 @@ public function testTraversableMap() $this->assertEquals(array('FOO', 'BAR'), iterable_to_array(iterable_map($iterable, $map))); } - /** - * @expectedException InvalidArgumentException - */ public function testInvalidIterable() { + $this->expectException(InvalidArgumentException::class); + $filter = function () { return true; }; diff --git a/tests/IterableToTraversableTest.php b/tests/IterableToTraversableTest.php index cb6f45f..b39c9cc 100644 --- a/tests/IterableToTraversableTest.php +++ b/tests/IterableToTraversableTest.php @@ -26,13 +26,11 @@ public function testArrayToTraversable() $this->assertInstanceOf('Traversable', $traversable); } - /** - * @expectedException InvalidArgumentException - */ public function testInvalidArgument() { + $this->expectException(InvalidArgumentException::class); + $string = 'foo'; iterable_to_traversable($string); - var_dump(iterable_to_traversable(array('foo', 'bar'))); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..b0ccdc7 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,7 @@ + Date: Sun, 17 Jan 2021 18:23:29 +0100 Subject: [PATCH 04/32] feat: apply specific namespace --- src/IterableObject.php | 88 +++-------- src/iterable-functions.php | 232 ++++++++++------------------ tests/IsIterableTest.php | 43 ------ tests/IterableFilterTest.php | 29 ++-- tests/IterableMapTest.php | 25 +-- tests/IterableObjectTest.php | 53 ++++--- tests/IterableReduceTest.php | 5 +- tests/IterableToArrayTest.php | 64 +++----- tests/IterableToTraversableTest.php | 23 +-- 9 files changed, 181 insertions(+), 381 deletions(-) delete mode 100644 tests/IsIterableTest.php diff --git a/src/IterableObject.php b/src/IterableObject.php index b8eec16..c4608ee 100644 --- a/src/IterableObject.php +++ b/src/IterableObject.php @@ -2,12 +2,13 @@ namespace BenTools\IterableFunctions; -use Closure; use EmptyIterator; -use InvalidArgumentException; use IteratorAggregate; use Traversable; +/** + * @internal + */ final class IterableObject implements IteratorAggregate { /** @@ -18,93 +19,50 @@ final class IterableObject implements IteratorAggregate /** * @var callable */ - private $filter; + private $filterFn; /** * @var callable */ - private $map; + private $mapFn; - /** - * IterableObject constructor. - * @param iterable|array|Traversable $iterable - * @param callable|null $filter - * @param callable|null $map - * @throws InvalidArgumentException - */ - public function __construct($iterable, $filter = null, $map = null) + public function __construct(?iterable $iterable = null, ?callable $filter = null, ?callable $map = null) { - if (null === $iterable) { - $iterable = new EmptyIterator(); - } - if (!is_iterable($iterable)) { - throw new InvalidArgumentException( - sprintf('Expected array or Traversable, got %s', is_object($iterable) ? get_class($iterable) : gettype($iterable)) - ); - } - - // Cannot rely on callable type-hint on PHP 5.3 - if (null !== $filter && !is_callable($filter) && !$filter instanceof Closure) { - throw new InvalidArgumentException( - sprintf('Expected callable, got %s', is_object($filter) ? get_class($filter) : gettype($filter)) - ); - } - - if (null !== $map && !is_callable($map) && !$map instanceof Closure) { - throw new InvalidArgumentException( - sprintf('Expected callable, got %s', is_object($map) ? get_class($map) : gettype($map)) - ); - } - - $this->iterable = $iterable; - $this->filter = $filter; - $this->map = $map; + $this->iterable = $iterable ?? new EmptyIterator(); + $this->filterFn = $filter; + $this->mapFn = $map; } - /** - * @param callable $filter - * @return self - */ - public function filter($filter) + public function filter(callable $filter): self { - return new self($this->iterable, $filter, $this->map); + return new self($this->iterable, $filter, $this->mapFn); } - /** - * @param callable $map - * @return self - */ - public function map($map) + public function map(callable $map): self { - return new self($this->iterable, $this->filter, $map); + return new self($this->iterable, $this->filterFn, $map); } - /** - * @inheritdoc - */ - public function getIterator() + public function getIterator(): Traversable { $iterable = $this->iterable; - if (null !== $this->filter) { - $iterable = iterable_filter($iterable, $this->filter); + if (null !== $this->filterFn) { + $iterable = iterable_filter($iterable, $this->filterFn); } - if (null !== $this->map) { - $iterable = iterable_map($iterable, $this->map); + if (null !== $this->mapFn) { + $iterable = iterable_map($iterable, $this->mapFn); } return iterable_to_traversable($iterable); } - /** - * @return array - */ - public function asArray() + public function asArray(): array { $iterable = $this->iterable; - if (null !== $this->filter) { - $iterable = iterable_filter($iterable, $this->filter); + if (null !== $this->filterFn) { + $iterable = iterable_filter($iterable, $this->filterFn); } - if (null !== $this->map) { - $iterable = iterable_map($iterable, $this->map); + if (null !== $this->mapFn) { + $iterable = iterable_map($iterable, $this->mapFn); } return iterable_to_array($iterable); } diff --git a/src/iterable-functions.php b/src/iterable-functions.php index 6fbdc9e..b92de9f 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -1,174 +1,108 @@ convert it to an ArrayIterator. + * + * @param array|Traversable $iterable + */ +function iterable_to_traversable(iterable $iterable): Traversable +{ + if ($iterable instanceof Traversable) { + return $iterable; } + + return new ArrayIterator($iterable); } -if (!function_exists('iterable_to_traversable')) { - - /** - * If the iterable is not intance of \Traversable, it is an array => convert it to an ArrayIterator. - * - * @param iterable|array|\Traversable $iterable - * @return \Traversable - */ - function iterable_to_traversable($iterable) - { - if ($iterable instanceof Traversable) { - return $iterable; - } elseif (is_array($iterable)) { - return new ArrayIterator($iterable); - } else { - throw new \InvalidArgumentException( - sprintf( - 'Expected array or \\Traversable, got %s', - is_object($iterable) ? get_class($iterable) : gettype($iterable) - ) - ); - } + +/** + * Filters an iterable. + * + * @param array|Traversable $iterable + * @return array|CallbackFilterIterator + */ +function iterable_filter(iterable $iterable, ?callable $filter = null) +{ + if (null === $filter) { + $filter = static function ($value) { + return (bool) $value; + }; } -} -if (!function_exists('iterable_filter')) { - - /** - * Filters an iterable. - * - * @param iterable|array|\Traversable $iterable - * @param callable $filter - * @return array|CallbackFilterIterator - * @throws InvalidArgumentException - */ - function iterable_filter($iterable, $filter = null) - { - if (!is_iterable($iterable)) { - throw new \InvalidArgumentException( - sprintf('Expected array or Traversable, got %s', is_object($iterable) ? get_class($iterable) : gettype($iterable)) - ); - } - - // Cannot rely on callable type-hint on PHP 5.3 - if (null !== $filter && !is_callable($filter) && !$filter instanceof Closure) { - throw new InvalidArgumentException( - sprintf('Expected callable, got %s', is_object($filter) ? get_class($filter) : gettype($filter)) - ); - } - - if (null === $filter) { - $filter = function ($value) { - return (bool) $value; - }; - } - - if ($iterable instanceof Traversable) { - if (!class_exists('CallbackFilterIterator')) { - throw new \RuntimeException('Class CallbackFilterIterator not found. Try using a polyfill, like symfony/polyfill-php54'); - } - return new CallbackFilterIterator(new IteratorIterator($iterable), $filter); - } - - return array_filter($iterable, $filter); + if ($iterable instanceof Traversable) { + return new CallbackFilterIterator(new IteratorIterator($iterable), $filter); } + return array_filter($iterable, $filter); } -if (!function_exists('iterable_reduce')) { - /** - * Reduces an iterable. - * - * @param iterable $iterable - * @param callable(mixed, mixed) $reduce - * @return mixed - * - * @psalm-template TValue - * @psalm-template TResult - * - * @psalm-param iterable $iterable - * @psalm-param callable(TResult|null, TValue) $reduce - * @psalm-param TResult|null $initial - * - * @psalm-return TResult|null - */ - function iterable_reduce($iterable, $reduce, $initial = null) - { - foreach ($iterable as $item) { - $initial = $reduce($initial, $item); - } - - return $initial; - } -} /** - * @param iterable|array|\Traversable $iterable - * @param callable|null $filter - * @param callable|null $map - * @return Traversable|IterableObject - * @throws InvalidArgumentException + * Reduces an iterable. + * + * @param iterable $iterable + * @param callable(mixed, mixed) $reduce + * @return mixed + * + * @psalm-template TValue + * @psalm-template TResult + * + * @psalm-param iterable $iterable + * @psalm-param callable(TResult|null, TValue) $reduce + * @psalm-param TResult|null $initial + * + * @psalm-return TResult|null */ -function iterable($iterable, $filter = null, $map = null) +function iterable_reduce(iterable $iterable, callable $reduce, $initial = null) +{ + foreach ($iterable as $item) { + $initial = $reduce($initial, $item); + } + + return $initial; +} + +function iterable(iterable $iterable, ?callable $filter = null, ?callable $map = null): IterableObject { return new IterableObject($iterable, $filter, $map); } diff --git a/tests/IsIterableTest.php b/tests/IsIterableTest.php deleted file mode 100644 index 7449010..0000000 --- a/tests/IsIterableTest.php +++ /dev/null @@ -1,43 +0,0 @@ -assertTrue(function_exists('is_iterable')); - } - - public function testArrayIsIterable() - { - $array = array('foo', 'bar'); - $this->assertTrue(is_iterable($array)); - } - - public function testIteratorIsIterable() - { - $iterator = new DirectoryIterator(__DIR__); - $this->assertTrue(is_iterable($iterator)); - } - - public function testScalarIsNotIterable() - { - $scalar = 'foobar'; - $this->assertFalse(is_iterable($scalar)); - } - - public function testObjectIsNotIterable() - { - $object = new \stdClass(); - $this->assertFalse(is_iterable($object)); - } - - public function testResourceIsNotIterable() - { - $resource = fopen('php://temp', 'rb'); - $this->assertFalse(is_iterable($resource)); - } - -} diff --git a/tests/IterableFilterTest.php b/tests/IterableFilterTest.php index fdc2539..36b7d38 100644 --- a/tests/IterableFilterTest.php +++ b/tests/IterableFilterTest.php @@ -1,35 +1,26 @@ assertEquals(array(1 => 'bar'), iterable_to_array(iterable_filter($iterable, $filter))); + $this->assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); } - public function testTraversableFilter() + public function testTraversableFilter(): void { - $iterable = SplFixedArray::fromArray(array('foo', 'bar')); - $filter = function ($input) { + $iterable = SplFixedArray::fromArray(['foo', 'bar']); + $filter = static function ($input) { return 'bar' === $input; }; - $this->assertEquals(array(1 => 'bar'), iterable_to_array(iterable_filter($iterable, $filter))); - } - - public function testInvalidIterable() - { - $this->expectException(InvalidArgumentException::class); - - $filter = function () { - return true; - }; - iterable_filter('foo', $filter); + $this->assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); } } diff --git a/tests/IterableMapTest.php b/tests/IterableMapTest.php index 0746827..0a0cda9 100644 --- a/tests/IterableMapTest.php +++ b/tests/IterableMapTest.php @@ -1,31 +1,22 @@ assertEquals(array('FOO', 'BAR'), iterable_to_array(iterable_map($iterable, $map))); + $this->assertEquals(['FOO', 'BAR'], iterable_to_array(iterable_map($iterable, $map))); } - public function testTraversableMap() + public function testTraversableMap(): void { - $iterable = SplFixedArray::fromArray(array('foo', 'bar')); + $iterable = SplFixedArray::fromArray(['foo', 'bar']); $map = 'strtoupper'; - $this->assertEquals(array('FOO', 'BAR'), iterable_to_array(iterable_map($iterable, $map))); - } - - public function testInvalidIterable() - { - $this->expectException(InvalidArgumentException::class); - - $filter = function () { - return true; - }; - iterable_map('foo', $filter); + $this->assertEquals(['FOO', 'BAR'], iterable_to_array(iterable_map($iterable, $map))); } } diff --git a/tests/IterableObjectTest.php b/tests/IterableObjectTest.php index 5c09d8c..0547cd6 100644 --- a/tests/IterableObjectTest.php +++ b/tests/IterableObjectTest.php @@ -1,84 +1,87 @@ assertInstanceOf('BenTools\IterableFunctions\IterableObject', $iterableObject); + $this->assertInstanceOf(IterableObject::class, $iterableObject); $this->assertEquals($expectedResult, iterator_to_array($iterableObject)); } + /** * @dataProvider dataProvider */ - public function testFromArrayToArray($data, $filter = null, $map = null, $expectedResult) + public function testFromArrayToArray($data, $filter = null, $map = null, $expectedResult): void { $iterableObject = iterable($data, $filter, $map); - $this->assertInstanceOf('BenTools\IterableFunctions\IterableObject', $iterableObject); + $this->assertInstanceOf(IterableObject::class, $iterableObject); $this->assertEquals($expectedResult, $iterableObject->asArray()); } /** * @dataProvider dataProvider */ - public function testFromTraversableToIterator($data, $filter = null, $map = null, $expectedResult) + public function testFromTraversableToIterator($data, $filter = null, $map = null, $expectedResult): void { $data = SplFixedArray::fromArray($data); $iterableObject = iterable($data, $filter, $map); - $this->assertInstanceOf('BenTools\IterableFunctions\IterableObject', $iterableObject); + $this->assertInstanceOf(IterableObject::class, $iterableObject); $this->assertEquals($expectedResult, iterator_to_array($iterableObject)); } + /** * @dataProvider dataProvider */ - public function testFromTraversableToArray($data, $filter = null, $map = null, $expectedResult) + public function testFromTraversableToArray($data, $filter = null, $map = null, $expectedResult): void { $data = SplFixedArray::fromArray($data); $iterableObject = iterable($data, $filter, $map); - $this->assertInstanceOf('BenTools\IterableFunctions\IterableObject', $iterableObject); + $this->assertInstanceOf(IterableObject::class, $iterableObject); $this->assertEquals($expectedResult, $iterableObject->asArray()); } public function dataProvider() { - $data = array('foo', 'bar'); - $filter = function ($value) { + $data = ['foo', 'bar']; + $filter = static function ($value) { return 'bar' === $value; }; $map = 'strtoupper'; - return array( - array( + return [ + [ $data, null, null, - array('foo', 'bar') - ), - array( + ['foo', 'bar'], + ], + [ $data, $filter, null, - array(1 => 'bar') - ), - array( + [1 => 'bar'], + ], + [ $data, null, $map, - array('FOO', 'BAR') - ), - array( + ['FOO', 'BAR'], + ], + [ $data, $filter, $map, - array(1 => 'BAR') - ), - ); + [1 => 'BAR'], + ], + ]; } } diff --git a/tests/IterableReduceTest.php b/tests/IterableReduceTest.php index c503629..78345d6 100644 --- a/tests/IterableReduceTest.php +++ b/tests/IterableReduceTest.php @@ -1,10 +1,11 @@ assertTrue(function_exists('iterable_to_array')); + $iterator = new ArrayIterator(['foo', 'bar']); + $this->assertEquals(['foo', 'bar'], iterable_to_array($iterator)); } - public function testIteratorToArray() + public function testIteratorWithoutKeysToArray(): void { - $iterator = new ArrayIterator(array('foo', 'bar')); - $this->assertEquals(array('foo', 'bar'), iterable_to_array($iterator)); + $iterator = new ArrayIterator([1 => 'foo', 2 => 'bar']); + $this->assertEquals([0 => 'foo', 1 => 'bar'], iterable_to_array($iterator, false)); } - public function testIteratorWithoutKeysToArray() + public function testArrayToArray(): void { - $iterator = new ArrayIterator(array(1 => 'foo', 2 => 'bar')); - $this->assertEquals(array(0 => 'foo', 1 => 'bar'), iterable_to_array($iterator, false)); + $array = ['foo', 'bar']; + $this->assertEquals(['foo', 'bar'], iterable_to_array($array)); } - public function testArrayToArray() + public function testArrayWithoutKeysToArray(): void { - $array = array('foo', 'bar'); - $this->assertEquals(array('foo', 'bar'), iterable_to_array($array)); + $array = [1 => 'foo', 2 => 'bar']; + $this->assertEquals([0 => 'foo', 1 => 'bar'], iterable_to_array($array, false)); } - public function testArrayWithoutKeysToArray() - { - $array = array(1 => 'foo', 2 => 'bar'); - $this->assertEquals(array(0 => 'foo', 1 => 'bar'), iterable_to_array($array, false)); - } - - public function testScalarToArray() + public function testScalarToArray(): void { $scalar = 'foobar'; $this->assertTrue($this->triggersError($scalar)); } - public function testObjectToArray() + public function testObjectToArray(): void { $object = new stdClass(); $this->assertTrue($this->triggersError($object)); } - public function testResourceToArray() + public function testResourceToArray(): void { $resource = fopen('php://temp', 'rb'); $this->assertTrue($this->triggersError($resource)); } - private function triggersError($input) - { - return version_compare(PHP_VERSION, '7.0.0') >= 0 ? $this->triggersErrorPHP7($input) : $this->triggersErrorPHP5($input); - } - - private function triggersErrorPHP7($input) + private function triggersError($input): bool { $errorOccured = false; try { iterable_to_array($input); - } - catch (\TypeError $e) { + } catch (\TypeError $e) { $errorOccured = true; } return $errorOccured; } - private function triggersErrorPHP5($input) - { - $errorOccured = false; - - set_error_handler(function ($errno) { - return E_RECOVERABLE_ERROR === $errno; - }); - - if (false === @iterable_to_array($input)) { - $errorOccured = true; - } - - restore_error_handler(); - - return $errorOccured; - } - } diff --git a/tests/IterableToTraversableTest.php b/tests/IterableToTraversableTest.php index b39c9cc..94be524 100644 --- a/tests/IterableToTraversableTest.php +++ b/tests/IterableToTraversableTest.php @@ -1,34 +1,27 @@ assertTrue(function_exists('iterable_to_traversable')); - } - - public function testIteratorToTraversable() + public function testIteratorToTraversable(): void { - $iterator = new ArrayIterator(array('foo' => 'bar')); + $iterator = new ArrayIterator(['foo' => 'bar']); $traversable = iterable_to_traversable($iterator); $this->assertSame($iterator, $traversable); - $this->assertInstanceOf('Traversable', $iterator); } - public function testArrayToTraversable() + public function testArrayToTraversable(): void { - $array = array('foo' => 'bar'); + $array = ['foo' => 'bar']; $traversable = iterable_to_traversable($array); - $this->assertEquals(new ArrayIterator(array('foo' => 'bar')), $traversable); - $this->assertInstanceOf('Traversable', $traversable); + $this->assertEquals(new ArrayIterator(['foo' => 'bar']), $traversable); } - public function testInvalidArgument() + public function testInvalidArgument(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(TypeError::class); $string = 'foo'; iterable_to_traversable($string); From 2f7c42064fb3db52db6ad1923f89982b189a768d Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Mon, 18 Jan 2021 12:12:12 +0100 Subject: [PATCH 05/32] Add sort-packages config to composer.json --- composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/composer.json b/composer.json index cca90ba..92f7375 100644 --- a/composer.json +++ b/composer.json @@ -21,5 +21,8 @@ "phpunit/phpunit": "^9", "squizlabs/php_codesniffer": "^2.0|^3.4", "symfony/var-dumper": "^5.2" + }, + "config": { + "sort-packages": true } } From 9c83f92e3954403f51277d9d399b8c0df84660fa Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Mon, 18 Jan 2021 12:14:13 +0100 Subject: [PATCH 06/32] Add Doctrine Coding standard --- .github/workflows/coding-standards.yml | 34 ++++++++++++++++++++++++++ .gitignore | 5 ++-- composer.json | 2 +- phpcs.xml.dist | 24 ++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/coding-standards.yml create mode 100644 phpcs.xml.dist diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml new file mode 100644 index 0000000..6007fbe --- /dev/null +++ b/.github/workflows/coding-standards.yml @@ -0,0 +1,34 @@ +name: "Coding Standards" + +on: + pull_request: + push: + branches: + - "master" + +jobs: + coding-standards: + name: "Coding Standards" + runs-on: "ubuntu-20.04" + + strategy: + matrix: + php-version: + - "7.3" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: "cs2pr" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v1" + + - name: "Run PHP_CodeSniffer" + run: "vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr" diff --git a/.gitignore b/.gitignore index c23da7c..7d21911 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -/vendor - +/.phpcs-cache /composer.lock +/phpcs.xml /phpunit.xml +/vendor/ diff --git a/composer.json b/composer.json index 92f7375..3b64988 100644 --- a/composer.json +++ b/composer.json @@ -18,8 +18,8 @@ "php": "^7.3 || ^8.0" }, "require-dev": { + "doctrine/coding-standard": "^8.2", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^2.0|^3.4", "symfony/var-dumper": "^5.2" }, "config": { diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..4a51616 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + src/ + tests/ + From d8ede8ced2641a3f619231cf62424c35691af8cf Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Mon, 18 Jan 2021 12:19:40 +0100 Subject: [PATCH 07/32] Fix CS --- composer.json | 3 ++ src/IterableObject.php | 33 ++++++++++++-------- src/iterable-functions.php | 48 +++++++++++++++++------------ tests/IterableFilterTest.php | 10 ++++-- tests/IterableMapTest.php | 6 ++++ tests/IterableObjectTest.php | 40 ++++++++++++++++-------- tests/IterableReduceTest.php | 10 ++++-- tests/IterableToArrayTest.php | 38 ++++------------------- tests/IterableToTraversableTest.php | 7 +++++ 9 files changed, 115 insertions(+), 80 deletions(-) diff --git a/composer.json b/composer.json index 3b64988..d9da434 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,9 @@ "files": ["src/iterable-functions.php"] }, "autoload-dev": { + "psr-4": { + "BenTools\\IterableFunctions\\Tests\\": "tests" + }, "files": [ "vendor/symfony/var-dumper/Resources/functions/dump.php" ] diff --git a/src/IterableObject.php b/src/IterableObject.php index c4608ee..2bd1560 100644 --- a/src/IterableObject.php +++ b/src/IterableObject.php @@ -1,5 +1,7 @@ */ private $iterable; - /** - * @var callable - */ + /** @var callable */ private $filterFn; - /** - * @var callable - */ + /** @var callable */ private $mapFn; + /** + * @param iterable|null $iterable + */ public function __construct(?iterable $iterable = null, ?callable $filter = null, ?callable $map = null) { $this->iterable = $iterable ?? new EmptyIterator(); @@ -43,27 +42,35 @@ public function map(callable $map): self return new self($this->iterable, $this->filterFn, $map); } + /** + * @return Traversable + */ public function getIterator(): Traversable { $iterable = $this->iterable; - if (null !== $this->filterFn) { + if ($this->filterFn !== null) { $iterable = iterable_filter($iterable, $this->filterFn); } - if (null !== $this->mapFn) { + + if ($this->mapFn !== null) { $iterable = iterable_map($iterable, $this->mapFn); } + return iterable_to_traversable($iterable); } + /** @return array */ public function asArray(): array { $iterable = $this->iterable; - if (null !== $this->filterFn) { + if ($this->filterFn !== null) { $iterable = iterable_filter($iterable, $this->filterFn); } - if (null !== $this->mapFn) { + + if ($this->mapFn !== null) { $iterable = iterable_map($iterable, $this->mapFn); } + return iterable_to_array($iterable); } } diff --git a/src/iterable-functions.php b/src/iterable-functions.php index b92de9f..207026f 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -1,18 +1,25 @@ $iterable + * + * @return iterable */ function iterable_map(iterable $iterable, callable $map): iterable { @@ -23,27 +30,29 @@ function iterable_map(iterable $iterable, callable $map): iterable return array_map($map, $iterable); } - /** * Copy the iterable into an array. If the iterable is already an array, return it. * - * @param array|Traversable $iterable - * @param bool $use_keys [optional] Whether to use the iterator element keys as index. - * @return array + * @param iterable $iterable + * @param bool $preserveKeys [optional] Whether to use the iterator element keys as index. + * + * @return array */ -function iterable_to_array(iterable $iterable, bool $use_keys = true): array +function iterable_to_array(iterable $iterable, bool $preserveKeys = true): array { if ($iterable instanceof Traversable) { - return iterator_to_array($iterable, $use_keys); + return iterator_to_array($iterable, $preserveKeys); } - return $use_keys ? $iterable : array_values($iterable); + return $preserveKeys ? $iterable : array_values($iterable); } /** - * If the iterable is not intance of Traversable, it is an array => convert it to an ArrayIterator. + * If the iterable is not instance of Traversable, it is an array => convert it to an ArrayIterator. + * + * @param iterable $iterable * - * @param array|Traversable $iterable + * @return Traversable */ function iterable_to_traversable(iterable $iterable): Traversable { @@ -54,16 +63,16 @@ function iterable_to_traversable(iterable $iterable): Traversable return new ArrayIterator($iterable); } - /** * Filters an iterable. * - * @param array|Traversable $iterable - * @return array|CallbackFilterIterator + * @param iterable $iterable + * + * @return array|CallbackFilterIterator */ function iterable_filter(iterable $iterable, ?callable $filter = null) { - if (null === $filter) { + if ($filter === null) { $filter = static function ($value) { return (bool) $value; }; @@ -76,21 +85,19 @@ function iterable_filter(iterable $iterable, ?callable $filter = null) return array_filter($iterable, $filter); } - /** * Reduces an iterable. * * @param iterable $iterable * @param callable(mixed, mixed) $reduce + * * @return mixed * * @psalm-template TValue * @psalm-template TResult - * * @psalm-param iterable $iterable * @psalm-param callable(TResult|null, TValue) $reduce * @psalm-param TResult|null $initial - * * @psalm-return TResult|null */ function iterable_reduce(iterable $iterable, callable $reduce, $initial = null) @@ -102,6 +109,9 @@ function iterable_reduce(iterable $iterable, callable $reduce, $initial = null) return $initial; } +/** + * @param iterable $iterable + */ function iterable(iterable $iterable, ?callable $filter = null, ?callable $map = null): IterableObject { return new IterableObject($iterable, $filter, $map); diff --git a/tests/IterableFilterTest.php b/tests/IterableFilterTest.php index 36b7d38..bbbe407 100644 --- a/tests/IterableFilterTest.php +++ b/tests/IterableFilterTest.php @@ -1,6 +1,12 @@ assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); } @@ -19,7 +25,7 @@ public function testTraversableFilter(): void { $iterable = SplFixedArray::fromArray(['foo', 'bar']); $filter = static function ($input) { - return 'bar' === $input; + return $input === 'bar'; }; $this->assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); } diff --git a/tests/IterableMapTest.php b/tests/IterableMapTest.php index 0a0cda9..c2dfbcd 100644 --- a/tests/IterableMapTest.php +++ b/tests/IterableMapTest.php @@ -1,6 +1,12 @@ $data + * @param array $expectedResult + * * @dataProvider dataProvider */ - public function testFromArrayToIterator($data, $filter = null, $map = null, $expectedResult): void + public function testFromArrayToIterator(array $data, ?callable $filter, ?string $map, array $expectedResult): void { $iterableObject = iterable($data, $filter, $map); - $this->assertInstanceOf(IterableObject::class, $iterableObject); $this->assertEquals($expectedResult, iterator_to_array($iterableObject)); } /** + * @param array $data + * @param array $expectedResult + * * @dataProvider dataProvider */ - public function testFromArrayToArray($data, $filter = null, $map = null, $expectedResult): void + public function testFromArrayToArray(array $data, ?callable $filter, ?string $map, array $expectedResult): void { $iterableObject = iterable($data, $filter, $map); - $this->assertInstanceOf(IterableObject::class, $iterableObject); $this->assertEquals($expectedResult, $iterableObject->asArray()); } /** + * @param array $data + * @param array $expectedResult + * * @dataProvider dataProvider */ - public function testFromTraversableToIterator($data, $filter = null, $map = null, $expectedResult): void + public function testFromTraversableToIterator(array $data, ?callable $filter, ?string $map, array $expectedResult): void { $data = SplFixedArray::fromArray($data); $iterableObject = iterable($data, $filter, $map); - $this->assertInstanceOf(IterableObject::class, $iterableObject); $this->assertEquals($expectedResult, iterator_to_array($iterableObject)); } /** + * @param array $data + * @param array $expectedResult + * * @dataProvider dataProvider */ - public function testFromTraversableToArray($data, $filter = null, $map = null, $expectedResult): void + public function testFromTraversableToArray(array $data, ?callable $filter, ?string $map, array $expectedResult): void { $data = SplFixedArray::fromArray($data); $iterableObject = iterable($data, $filter, $map); - $this->assertInstanceOf(IterableObject::class, $iterableObject); $this->assertEquals($expectedResult, $iterableObject->asArray()); } - public function dataProvider() + /** + * @return list, callable|null, string|null, array}>> + */ + public function dataProvider(): array { $data = ['foo', 'bar']; $filter = static function ($value) { - return 'bar' === $value; + return $value === 'bar'; }; $map = 'strtoupper'; @@ -83,5 +100,4 @@ public function dataProvider() ], ]; } - } diff --git a/tests/IterableReduceTest.php b/tests/IterableReduceTest.php index 78345d6..5c7abc5 100644 --- a/tests/IterableReduceTest.php +++ b/tests/IterableReduceTest.php @@ -1,13 +1,19 @@ 'foo', 2 => 'bar']; $this->assertEquals([0 => 'foo', 1 => 'bar'], iterable_to_array($array, false)); } - - public function testScalarToArray(): void - { - $scalar = 'foobar'; - $this->assertTrue($this->triggersError($scalar)); - } - - public function testObjectToArray(): void - { - $object = new stdClass(); - $this->assertTrue($this->triggersError($object)); - } - - public function testResourceToArray(): void - { - $resource = fopen('php://temp', 'rb'); - $this->assertTrue($this->triggersError($resource)); - } - - private function triggersError($input): bool - { - $errorOccured = false; - - try { - iterable_to_array($input); - } catch (\TypeError $e) { - $errorOccured = true; - } - - return $errorOccured; - } - } diff --git a/tests/IterableToTraversableTest.php b/tests/IterableToTraversableTest.php index 94be524..9e3d8ec 100644 --- a/tests/IterableToTraversableTest.php +++ b/tests/IterableToTraversableTest.php @@ -1,6 +1,13 @@ Date: Sat, 23 Jan 2021 10:46:06 +0100 Subject: [PATCH 08/32] docs: update documentation --- README.md | 93 ++++++++++++++++++++++++------------------------------- 1 file changed, 40 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 74aedf6..032ba4d 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,6 @@ Iterable functions Provides additional functions to work with [iterable](https://wiki.php.net/rfc/iterable) variables (even on PHP5.3+). -is_iterable() -------------- -To check wether or not a PHP variable can be looped over in a `foreach` statement, PHP provides an `is_iterable()` function. - -**But this function only works on PHP7.1+**. - -This library ships a polyfill of this function for previous PHP versions. - -Usage: -```php -var_dump(is_iterable(array('foo', 'bar'))); // true -var_dump(is_iterable(new DirectoryIterator(__DIR__))); // true -var_dump(is_iterable('foobar')); // false -``` - iterable_to_array() ------------------- @@ -30,13 +15,15 @@ PHP offers an `iterator_to_array()` function to export any iterator into an arra **But when you want to transform an `iterable` to an array, the `iterable` itself can already be an array.** -When using `iterator_to_array()` with an array, PHP5 triggers a E_RECOVERABLE_ERROR while PHP7 throws a `TypeError`. +When using `iterator_to_array()` with an iterable, that happens to be an array, PHP will throw a `TypeError`. If you need an iterable-agnostic function, try our `iterable_to_array()`: ```php -var_dump(iterable_to_array(new ArrayIterator(array('foo', 'bar')))); // ['foo', 'bar'] -var_dump(iterable_to_array(array('foo', 'bar'))); // ['foo', 'bar'] +use function BenTools\IterableFunctions\iterable_to_array; + +var_dump(iterable_to_array(new \ArrayIterator(['foo', 'bar']))); // ['foo', 'bar'] +var_dump(iterable_to_array(['foo', 'bar'])); // ['foo', 'bar'] ``` iterable_to_traversable() @@ -49,8 +36,10 @@ If your variable is an array, the function converts it to an `ArrayIterator`. Usage: ```php -var_dump(iterable_to_traversable(array('foo', 'bar'))); // ArrayIterator(array('foo', 'bar')) -var_dump(iterable_to_traversable(new ArrayIterator(array('foo', 'bar')))); // ArrayIterator(array('foo', 'bar')) +use function BenTools\IterableFunctions\iterable_to_traversable; + +var_dump(iterable_to_traversable(['foo', 'bar'])); // \ArrayIterator(['foo', 'bar']) +var_dump(iterable_to_traversable(new \ArrayIterator(['foo', 'bar']))); // \ArrayIterator(['foo', 'bar']) ``` @@ -60,6 +49,8 @@ iterable_map() Works like an `array_map` with an `array` or a `Traversable`. ```php +use function BenTools\IterableFunctions\iterable_map; + $generator = function () { yield 'foo'; yield 'bar'; @@ -76,6 +67,8 @@ iterable_reduce() Works like an `reduce` with an `iterable`. ```php +use function BenTools\IterableFunctions\iterable_reduce; + $generator = function () { yield 1; yield 2; @@ -96,6 +89,8 @@ iterable_filter() Works like an `array_filter` with an `array` or a `Traversable`. ```php +use function BenTools\IterableFunctions\iterable_filter; + $generator = function () { yield 0; yield 1; @@ -108,6 +103,8 @@ foreach (iterable_filter($generator()) as $item) { Of course you can define your own filter: ```php +use function BenTools\IterableFunctions\iterable_filter; + $generator = function () { yield 'foo'; yield 'bar'; @@ -123,54 +120,43 @@ foreach (iterable_filter($generator(), $filter) as $item) { } ``` +Iterable fluent interface +========================= -Iterable factory -================ - -When you have an `iterable` type-hint somewhere, and don't know in advance wether you'll pass an `array` or a `Traversable`, just call the magic `iterable()` factory: +The `iterable` function allows you to wrap an iterable and apply some common operations. +With an array input: ```php -interface SomeInterface -{ - /** - * Return an iterable list of items - * - * @return iterable - */ - public function getItems(): iterable; -} - -class MyService implements SomeInterface -{ - /** - * @inheritdoc - */ - public function getItems(): iterable - { - return iterable($this->someOtherService->findAll()): - } +use function BenTools\IterableFunctions\iterable; +$data = [ + 'banana', + 'pineapple', + 'rock', +]; -} +$iterable = iterable($data)->filter(fn($eatable) => 'rock' !== $eatable)->map('strtoupper'); // Traversable of ['banana', 'pineapple'] ``` -It even accepts a `null` value (then converting it to an `EmptyIterator`). - -You may add a `filter` callable and a `map` callable to make your life easier: - +With a traversable input: ```php +use function BenTools\IterableFunctions\iterable; $data = [ 'banana', 'pineapple', - 'potato', + 'rock', ]; -$isFruit = function ($eatable) { - return 'potato' !== $eatable; -}; +$data = fn() => yield from $data; -var_dump(iterator_to_array(iterable($data)->filter($isFruit)->map('strtoupper'))); // ['banana', 'pineapple'] +$iterable = iterable($data())->filter(fn($eatable) => 'rock' !== $eatable)->map('strtoupper'); // Traversable of ['banana', 'pineapple'] ``` +Array output: +```php +$iterable->asArray(); // array ['banana', 'pineapple'] +``` + + Installation ============ @@ -186,6 +172,7 @@ require_once '/path/to/this/library/src/iterable-functions.php'; Unit tests ========== + ``` ./vendor/bin/phpunit ``` From 1162238c925b3b12bc067966dd94cf23349fb397 Mon Sep 17 00:00:00 2001 From: Beno!t POLASZEK Date: Sun, 24 Jan 2021 18:59:34 +0100 Subject: [PATCH 09/32] Refactor: Use pest for tests (#14) --- .github/workflows/continuous-integration.yml | 14 +- README.md | 2 +- composer.json | 2 +- phpcs.xml.dist | 1 + tests/IterableFilterTest.php | 48 +++--- tests/IterableMapTest.php | 28 ++- tests/IterableObjectTest.php | 171 +++++++++---------- tests/IterableReduceTest.php | 36 ++-- tests/IterableToArrayTest.php | 51 +++--- tests/IterableToTraversableTest.php | 42 ++--- 10 files changed, 187 insertions(+), 208 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 799014e..2e5ad90 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -7,8 +7,8 @@ on: - cron: "0 0 1 * *" jobs: - phpunit: - name: "PHPUnit" + unit-tests: + name: "Unit Tests" runs-on: "ubuntu-20.04" strategy: @@ -41,20 +41,20 @@ jobs: with: dependency-versions: "${{ matrix.dependencies }}" - - name: "Run PHPUnit" - run: "vendor/bin/phpunit --coverage-clover=coverage.xml" + - name: "Run unit tests" + run: "vendor/bin/pest --coverage-clover=coverage.xml" - name: "Upload coverage file" uses: "actions/upload-artifact@v2" with: - name: "phpunit-${{ matrix.deps }}-${{ matrix.php-version }}.coverage" + name: "pest-${{ matrix.deps }}-${{ matrix.php-version }}.coverage" path: "coverage.xml" - upload_coverage: + upload-coverage: name: "Upload coverage to Codecov" runs-on: "ubuntu-20.04" needs: - - "phpunit" + - "unit-tests" steps: - name: "Checkout" diff --git a/README.md b/README.md index 032ba4d..890c809 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ Unit tests ========== ``` -./vendor/bin/phpunit +php vendor/bin/pest ``` [CodeCov Master]: https://codecov.io/gh/bpolaszek/php-iterable-functions/branch/2.0.x-dev diff --git a/composer.json b/composer.json index d9da434..28df60a 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ }, "require-dev": { "doctrine/coding-standard": "^8.2", - "phpunit/phpunit": "^9", + "pestphp/pest": "^1.0", "symfony/var-dumper": "^5.2" }, "config": { diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 4a51616..82c4a0e 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -17,6 +17,7 @@ + src/ diff --git a/tests/IterableFilterTest.php b/tests/IterableFilterTest.php index bbbe407..394aae2 100644 --- a/tests/IterableFilterTest.php +++ b/tests/IterableFilterTest.php @@ -4,29 +4,35 @@ namespace BenTools\IterableFunctions\Tests; -use PHPUnit\Framework\TestCase; use SplFixedArray; use function BenTools\IterableFunctions\iterable_filter; use function BenTools\IterableFunctions\iterable_to_array; +use function it; +use function PHPUnit\Framework\assertEquals; -final class IterableFilterTest extends TestCase -{ - public function testArrayFilter(): void - { - $iterable = ['foo', 'bar']; - $filter = static function ($input) { - return $input === 'bar'; - }; - $this->assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); - } - - public function testTraversableFilter(): void - { - $iterable = SplFixedArray::fromArray(['foo', 'bar']); - $filter = static function ($input) { - return $input === 'bar'; - }; - $this->assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); - } -} +it('filters an array', function (): void { + $iterable = [false, true]; + assertEquals([1 => true], iterable_to_array(iterable_filter($iterable))); +}); + +it('filters a Travsersable object', function (): void { + $iterable = SplFixedArray::fromArray([false, true]); + assertEquals([1 => true], iterable_to_array(iterable_filter($iterable))); +}); + +it('filters an array with a callback', function (): void { + $iterable = ['foo', 'bar']; + $filter = static function ($input) { + return $input === 'bar'; + }; + assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); +}); + +it('filters a Travsersable object with a callback', function (): void { + $iterable = SplFixedArray::fromArray(['foo', 'bar']); + $filter = static function ($input) { + return $input === 'bar'; + }; + assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); +}); diff --git a/tests/IterableMapTest.php b/tests/IterableMapTest.php index c2dfbcd..c9b7c99 100644 --- a/tests/IterableMapTest.php +++ b/tests/IterableMapTest.php @@ -4,25 +4,21 @@ namespace BenTools\IterableFunctions\Tests; -use PHPUnit\Framework\TestCase; use SplFixedArray; use function BenTools\IterableFunctions\iterable_map; use function BenTools\IterableFunctions\iterable_to_array; +use function it; +use function PHPUnit\Framework\assertEquals; -final class IterableMapTest extends TestCase -{ - public function testArrayMap(): void - { - $iterable = ['foo', 'bar']; - $map = 'strtoupper'; - $this->assertEquals(['FOO', 'BAR'], iterable_to_array(iterable_map($iterable, $map))); - } +it('maps an array', function (): void { + $iterable = ['foo', 'bar']; + $map = 'strtoupper'; + assertEquals(['FOO', 'BAR'], iterable_to_array(iterable_map($iterable, $map))); +}); - public function testTraversableMap(): void - { - $iterable = SplFixedArray::fromArray(['foo', 'bar']); - $map = 'strtoupper'; - $this->assertEquals(['FOO', 'BAR'], iterable_to_array(iterable_map($iterable, $map))); - } -} +it('maps a Traversable object', function (): void { + $iterable = SplFixedArray::fromArray(['foo', 'bar']); + $map = 'strtoupper'; + assertEquals(['FOO', 'BAR'], iterable_to_array(iterable_map($iterable, $map))); +}); diff --git a/tests/IterableObjectTest.php b/tests/IterableObjectTest.php index b5c2f53..5812a2a 100644 --- a/tests/IterableObjectTest.php +++ b/tests/IterableObjectTest.php @@ -4,100 +4,97 @@ namespace BenTools\IterableFunctions\Tests; -use PHPUnit\Framework\TestCase; +use BenTools\IterableFunctions\IterableObject; use SplFixedArray; use function BenTools\IterableFunctions\iterable; +use function it; use function iterator_to_array; +use function PHPUnit\Framework\assertEquals; +use function PHPUnit\Framework\assertInstanceOf; +use function test; -final class IterableObjectTest extends TestCase -{ - /** - * @param array $data - * @param array $expectedResult - * - * @dataProvider dataProvider - */ - public function testFromArrayToIterator(array $data, ?callable $filter, ?string $map, array $expectedResult): void - { - $iterableObject = iterable($data, $filter, $map); - $this->assertEquals($expectedResult, iterator_to_array($iterableObject)); - } +$dataProvider = function () { + $data = ['foo', 'bar']; + $filter = static function ($value) { + return $value === 'bar'; + }; + $map = 'strtoupper'; - /** - * @param array $data - * @param array $expectedResult - * - * @dataProvider dataProvider - */ - public function testFromArrayToArray(array $data, ?callable $filter, ?string $map, array $expectedResult): void - { - $iterableObject = iterable($data, $filter, $map); - $this->assertEquals($expectedResult, $iterableObject->asArray()); - } + yield from [ + [ + $data, + null, + null, + ['foo', 'bar'], + ], + [ + $data, + $filter, + null, + [1 => 'bar'], + ], + [ + $data, + null, + $map, + ['FOO', 'BAR'], + ], + [ + $data, + $filter, + $map, + [1 => 'BAR'], + ], + ]; +}; - /** - * @param array $data - * @param array $expectedResult - * - * @dataProvider dataProvider - */ - public function testFromTraversableToIterator(array $data, ?callable $filter, ?string $map, array $expectedResult): void - { - $data = SplFixedArray::fromArray($data); - $iterableObject = iterable($data, $filter, $map); - $this->assertEquals($expectedResult, iterator_to_array($iterableObject)); - } +test('input: array | output: traversable', function ($data, $filter, $map, $expectedResult): void { + $iterableObject = iterable($data, $filter, $map); + assertEquals($expectedResult, iterator_to_array($iterableObject)); +})->with($dataProvider()); - /** - * @param array $data - * @param array $expectedResult - * - * @dataProvider dataProvider - */ - public function testFromTraversableToArray(array $data, ?callable $filter, ?string $map, array $expectedResult): void - { - $data = SplFixedArray::fromArray($data); - $iterableObject = iterable($data, $filter, $map); - $this->assertEquals($expectedResult, $iterableObject->asArray()); - } +test('input: array | output: array', function ($data, $filter, $map, $expectedResult): void { + $iterableObject = iterable($data, $filter, $map); + assertEquals($expectedResult, $iterableObject->asArray()); +})->with($dataProvider()); - /** - * @return list, callable|null, string|null, array}>> - */ - public function dataProvider(): array - { - $data = ['foo', 'bar']; - $filter = static function ($value) { - return $value === 'bar'; - }; - $map = 'strtoupper'; +test('input: traversable | output: traversable', function ($data, $filter, $map, $expectedResult): void { + $data = SplFixedArray::fromArray($data); + $iterableObject = iterable($data, $filter, $map); + assertEquals($expectedResult, iterator_to_array($iterableObject)); +})->with($dataProvider()); - return [ - [ - $data, - null, - null, - ['foo', 'bar'], - ], - [ - $data, - $filter, - null, - [1 => 'bar'], - ], - [ - $data, - null, - $map, - ['FOO', 'BAR'], - ], - [ - $data, - $filter, - $map, - [1 => 'BAR'], - ], - ]; - } -} +test('input: traversable | output: array', function ($data, $filter, $map, $expectedResult): void { + $data = SplFixedArray::fromArray($data); + $iterableObject = iterable($data, $filter, $map); + assertEquals($expectedResult, $iterableObject->asArray()); +})->with($dataProvider()); + +it('filters the subject', function (): void { + $filter = static function ($value) { + return $value === 'bar'; + }; + $iterableObject = iterable(['foo', 'bar'])->filter($filter); + assertEquals([1 => 'bar'], iterator_to_array($iterableObject)); +}); + +it('maps the subject', function (): void { + $map = 'strtoupper'; + $iterableObject = iterable(['foo', 'bar'])->map($map); + assertInstanceOf(IterableObject::class, $iterableObject); + assertEquals(['FOO', 'BAR'], iterator_to_array($iterableObject)); +}); + +it('combines filter and map', function (): void { + $filter = static function ($value) { + return $value === 'bar'; + }; + $map = 'strtoupper'; + $iterableObject = iterable(['foo', 'bar'])->map($map)->filter($filter); + assertInstanceOf(IterableObject::class, $iterableObject); + assertEquals([1 => 'BAR'], iterator_to_array($iterableObject)); + $iterableObject = iterable(['foo', 'bar'])->filter($filter)->map($map); + assertInstanceOf(IterableObject::class, $iterableObject); + assertEquals([1 => 'BAR'], iterator_to_array($iterableObject)); +}); diff --git a/tests/IterableReduceTest.php b/tests/IterableReduceTest.php index 5c7abc5..f6ee50d 100644 --- a/tests/IterableReduceTest.php +++ b/tests/IterableReduceTest.php @@ -4,28 +4,24 @@ namespace BenTools\IterableFunctions\Tests; -use PHPUnit\Framework\TestCase; use SplFixedArray; use function BenTools\IterableFunctions\iterable_reduce; +use function it; +use function PHPUnit\Framework\assertSame; -final class IterableReduceTest extends TestCase -{ - public function testArrayReduce(): void - { - $iterable = [1, 2]; - $reduce = static function ($carry, $item) { - return $carry + $item; - }; - self::assertSame(3, iterable_reduce($iterable, $reduce, 0)); - } +it('reduces an array', function (): void { + $iterable = [1, 2]; + $reduce = static function ($carry, $item) { + return $carry + $item; + }; + assertSame(3, iterable_reduce($iterable, $reduce, 0)); +}); - public function testTraversableReduce(): void - { - $iterable = SplFixedArray::fromArray([1, 2]); - $reduce = static function ($carry, $item) { - return $carry + $item; - }; - self::assertSame(3, iterable_reduce($iterable, $reduce, 0)); - } -} +it('reduces an traversable', function (): void { + $iterable = SplFixedArray::fromArray([1, 2]); + $reduce = static function ($carry, $item) { + return $carry + $item; + }; + assertSame(3, iterable_reduce($iterable, $reduce, 0)); +}); diff --git a/tests/IterableToArrayTest.php b/tests/IterableToArrayTest.php index e139192..0268686 100644 --- a/tests/IterableToArrayTest.php +++ b/tests/IterableToArrayTest.php @@ -5,33 +5,28 @@ namespace BenTools\IterableFunctions\Tests; use ArrayIterator; -use PHPUnit\Framework\TestCase; use function BenTools\IterableFunctions\iterable_to_array; - -final class IterableToArrayTest extends TestCase -{ - public function testIteratorToArray(): void - { - $iterator = new ArrayIterator(['foo', 'bar']); - $this->assertEquals(['foo', 'bar'], iterable_to_array($iterator)); - } - - public function testIteratorWithoutKeysToArray(): void - { - $iterator = new ArrayIterator([1 => 'foo', 2 => 'bar']); - $this->assertEquals([0 => 'foo', 1 => 'bar'], iterable_to_array($iterator, false)); - } - - public function testArrayToArray(): void - { - $array = ['foo', 'bar']; - $this->assertEquals(['foo', 'bar'], iterable_to_array($array)); - } - - public function testArrayWithoutKeysToArray(): void - { - $array = [1 => 'foo', 2 => 'bar']; - $this->assertEquals([0 => 'foo', 1 => 'bar'], iterable_to_array($array, false)); - } -} +use function it; +use function PHPUnit\Framework\assertEquals; +use function PHPUnit\Framework\assertSame; + +it('converts an iterator to an array', function (): void { + $iterator = new ArrayIterator(['foo', 'bar']); + assertEquals(['foo', 'bar'], iterable_to_array($iterator)); +}); + +it('converts an iterator to an array, without keys', function (): void { + $iterator = new ArrayIterator([1 => 'foo', 2 => 'bar']); + assertEquals([0 => 'foo', 1 => 'bar'], iterable_to_array($iterator, false)); +}); + +it('keeps the same array', function (): void { + $array = ['foo', 'bar']; + assertSame(['foo', 'bar'], iterable_to_array($array)); +}); + +it('removes the keys of an array', function (): void { + $array = [1 => 'foo', 2 => 'bar']; + assertEquals([0 => 'foo', 1 => 'bar'], iterable_to_array($array, false)); +}); diff --git a/tests/IterableToTraversableTest.php b/tests/IterableToTraversableTest.php index 9e3d8ec..688aa57 100644 --- a/tests/IterableToTraversableTest.php +++ b/tests/IterableToTraversableTest.php @@ -5,32 +5,20 @@ namespace BenTools\IterableFunctions\Tests; use ArrayIterator; -use PHPUnit\Framework\TestCase; -use TypeError; use function BenTools\IterableFunctions\iterable_to_traversable; - -final class IterableToTraversableTest extends TestCase -{ - public function testIteratorToTraversable(): void - { - $iterator = new ArrayIterator(['foo' => 'bar']); - $traversable = iterable_to_traversable($iterator); - $this->assertSame($iterator, $traversable); - } - - public function testArrayToTraversable(): void - { - $array = ['foo' => 'bar']; - $traversable = iterable_to_traversable($array); - $this->assertEquals(new ArrayIterator(['foo' => 'bar']), $traversable); - } - - public function testInvalidArgument(): void - { - $this->expectException(TypeError::class); - - $string = 'foo'; - iterable_to_traversable($string); - } -} +use function it; +use function PHPUnit\Framework\assertEquals; +use function PHPUnit\Framework\assertSame; + +it('keeps the same traversable object', function (): void { + $iterator = new ArrayIterator(['foo' => 'bar']); + $traversable = iterable_to_traversable($iterator); + assertSame($iterator, $traversable); +}); + +it('converts an array to a traversable object', function (): void { + $array = ['foo' => 'bar']; + $traversable = iterable_to_traversable($array); + assertEquals(new ArrayIterator(['foo' => 'bar']), $traversable); +}); From dcabd1a42300225a2c94dfd3de77993e17d9b04d Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Tue, 26 Jan 2021 12:25:13 +0100 Subject: [PATCH 10/32] Feat: Add PHPStan (#19) --- .github/workflows/static-analysis.yml | 34 +++++++++++++++++++++++++++ .gitignore | 2 ++ composer.json | 3 +++ phpstan.neon.dist | 5 ++++ src/IterableObject.php | 6 +++-- src/iterable-functions.php | 8 +++---- tests/IterableFilterTest.php | 6 ++--- tests/IterableObjectTest.php | 9 +++---- 8 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/static-analysis.yml create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..862be75 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,34 @@ +name: "Static Analysis" + +on: + pull_request: + push: + branches: + - "master" + +jobs: + static-analysis-phpstan: + name: "Static Analysis with PHPStan" + runs-on: "ubuntu-20.04" + + strategy: + matrix: + php-version: + - "7.3" + + steps: + - name: "Checkout code" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: "cs2pr" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v1" + + - name: "Run a static analysis with phpstan/phpstan" + run: "vendor/bin/phpstan analyse --error-format=checkstyle | cs2pr" diff --git a/.gitignore b/.gitignore index 7d21911..6f24d73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /.phpcs-cache +/.phpunit.result.cache /composer.lock /phpcs.xml +/phpstan.neon /phpunit.xml /vendor/ diff --git a/composer.json b/composer.json index 28df60a..b6a5021 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,9 @@ "require-dev": { "doctrine/coding-standard": "^8.2", "pestphp/pest": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^0.12.67", + "phpstan/phpstan-strict-rules": "^0.12.9", "symfony/var-dumper": "^5.2" }, "config": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..8ea502f --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,5 @@ +parameters: + level: max + paths: + - %currentWorkingDirectory%/src + - %currentWorkingDirectory%/tests diff --git a/src/IterableObject.php b/src/IterableObject.php index 2bd1560..3a07b04 100644 --- a/src/IterableObject.php +++ b/src/IterableObject.php @@ -10,16 +10,18 @@ /** * @internal + * + * @implements IteratorAggregate */ final class IterableObject implements IteratorAggregate { /** @var iterable */ private $iterable; - /** @var callable */ + /** @var callable|null */ private $filterFn; - /** @var callable */ + /** @var callable|null */ private $mapFn; /** diff --git a/src/iterable-functions.php b/src/iterable-functions.php index 207026f..c35732c 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -73,7 +73,7 @@ function iterable_to_traversable(iterable $iterable): Traversable function iterable_filter(iterable $iterable, ?callable $filter = null) { if ($filter === null) { - $filter = static function ($value) { + $filter = static function ($value): bool { return (bool) $value; }; } @@ -89,14 +89,14 @@ function iterable_filter(iterable $iterable, ?callable $filter = null) * Reduces an iterable. * * @param iterable $iterable - * @param callable(mixed, mixed) $reduce + * @param callable(mixed, mixed):mixed $reduce * * @return mixed * * @psalm-template TValue - * @psalm-template TResult + * @template TResult * @psalm-param iterable $iterable - * @psalm-param callable(TResult|null, TValue) $reduce + * @psalm-param callable(TResult|null, TValue):TResult $reduce * @psalm-param TResult|null $initial * @psalm-return TResult|null */ diff --git a/tests/IterableFilterTest.php b/tests/IterableFilterTest.php index 394aae2..083d355 100644 --- a/tests/IterableFilterTest.php +++ b/tests/IterableFilterTest.php @@ -16,14 +16,14 @@ assertEquals([1 => true], iterable_to_array(iterable_filter($iterable))); }); -it('filters a Travsersable object', function (): void { +it('filters a Traversable object', function (): void { $iterable = SplFixedArray::fromArray([false, true]); assertEquals([1 => true], iterable_to_array(iterable_filter($iterable))); }); it('filters an array with a callback', function (): void { $iterable = ['foo', 'bar']; - $filter = static function ($input) { + $filter = static function ($input): bool { return $input === 'bar'; }; assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); @@ -31,7 +31,7 @@ it('filters a Travsersable object with a callback', function (): void { $iterable = SplFixedArray::fromArray(['foo', 'bar']); - $filter = static function ($input) { + $filter = static function ($input): bool { return $input === 'bar'; }; assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); diff --git a/tests/IterableObjectTest.php b/tests/IterableObjectTest.php index 5812a2a..f7dc96b 100644 --- a/tests/IterableObjectTest.php +++ b/tests/IterableObjectTest.php @@ -5,6 +5,7 @@ namespace BenTools\IterableFunctions\Tests; use BenTools\IterableFunctions\IterableObject; +use Generator; use SplFixedArray; use function BenTools\IterableFunctions\iterable; @@ -14,9 +15,9 @@ use function PHPUnit\Framework\assertInstanceOf; use function test; -$dataProvider = function () { +$dataProvider = static function (): Generator { $data = ['foo', 'bar']; - $filter = static function ($value) { + $filter = static function ($value): bool { return $value === 'bar'; }; $map = 'strtoupper'; @@ -72,7 +73,7 @@ })->with($dataProvider()); it('filters the subject', function (): void { - $filter = static function ($value) { + $filter = static function ($value): bool { return $value === 'bar'; }; $iterableObject = iterable(['foo', 'bar'])->filter($filter); @@ -87,7 +88,7 @@ }); it('combines filter and map', function (): void { - $filter = static function ($value) { + $filter = static function ($value): bool { return $value === 'bar'; }; $map = 'strtoupper'; From 93b26475fde1ab67e5fb53b7c9b424c5b832bb2a Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Tue, 26 Jan 2021 20:54:46 +0100 Subject: [PATCH 11/32] Docs: Autoformat Readme (#21) --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 890c809..d092f8a 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ If your variable is already an instance of `Traversable` (i.e. an `Iterator`, an If your variable is an array, the function converts it to an `ArrayIterator`. Usage: + ```php use function BenTools\IterableFunctions\iterable_to_traversable; @@ -42,7 +43,6 @@ var_dump(iterable_to_traversable(['foo', 'bar'])); // \ArrayIterator(['foo', 'ba var_dump(iterable_to_traversable(new \ArrayIterator(['foo', 'bar']))); // \ArrayIterator(['foo', 'bar']) ``` - iterable_map() -------------- @@ -102,6 +102,7 @@ foreach (iterable_filter($generator()) as $item) { ``` Of course you can define your own filter: + ```php use function BenTools\IterableFunctions\iterable_filter; @@ -126,6 +127,7 @@ Iterable fluent interface The `iterable` function allows you to wrap an iterable and apply some common operations. With an array input: + ```php use function BenTools\IterableFunctions\iterable; $data = [ @@ -138,6 +140,7 @@ $iterable = iterable($data)->filter(fn($eatable) => 'rock' !== $eatable)->map('s ``` With a traversable input: + ```php use function BenTools\IterableFunctions\iterable; $data = [ @@ -152,20 +155,22 @@ $iterable = iterable($data())->filter(fn($eatable) => 'rock' !== $eatable)->map( ``` Array output: + ```php $iterable->asArray(); // array ['banana', 'pineapple'] ``` - Installation ============ With composer (they'll be autoloaded): + ``` composer require bentools/iterable-functions ``` Or manually: + ```php require_once '/path/to/this/library/src/iterable-functions.php'; ``` From 5d1b9928d700f75a946e349309e4c86453427677 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Tue, 26 Jan 2021 21:01:18 +0100 Subject: [PATCH 12/32] Feat: Add Psalm (#20) --- .github/workflows/static-analysis.yml | 24 +++++++++ .gitignore | 1 + README.md | 12 ++++- composer.json | 3 +- psalm.xml.dist | 27 ++++++++++ src/iterable-functions.php | 8 +-- tests/IterableFilterTest.php | 8 ++- tests/IterableObjectTest.php | 76 +++++++++++++++++---------- tests/IterableReduceTest.php | 8 +-- 9 files changed, 128 insertions(+), 39 deletions(-) create mode 100644 psalm.xml.dist diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 862be75..f673d86 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -32,3 +32,27 @@ jobs: - name: "Run a static analysis with phpstan/phpstan" run: "vendor/bin/phpstan analyse --error-format=checkstyle | cs2pr" + + static-analysis-psalm: + name: "Static Analysis with Psalm" + runs-on: "ubuntu-20.04" + + strategy: + matrix: + php-version: + - "7.3" + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Psalm + uses: docker://vimeo/psalm-github-actions:4.4.1 + with: + composer_require_dev: true + security_analysis: true + report_file: results.sarif + - name: Upload Security Analysis results to GitHub + uses: github/codeql-action/upload-sarif@v1 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index 6f24d73..56ed85a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ /composer.lock /phpcs.xml /phpstan.neon +/psalm.xml /phpunit.xml /vendor/ diff --git a/README.md b/README.md index d092f8a..0b91fdd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![Latest Stable Version](https://poser.pugx.org/bentools/iterable-functions/v/stable)](https://packagist.org/packages/bentools/iterable-functions) [![GitHub Actions][GA master image]][GA master] [![Code Coverage][Coverage image]][CodeCov Master] +[![Shepherd Type][Shepherd Image]][Shepherd Link] [![Total Downloads](https://poser.pugx.org/bentools/iterable-functions/downloads)](https://packagist.org/packages/bentools/iterable-functions) Iterable functions @@ -182,7 +183,14 @@ Unit tests php vendor/bin/pest ``` -[CodeCov Master]: https://codecov.io/gh/bpolaszek/php-iterable-functions/branch/2.0.x-dev -[Coverage image]: https://codecov.io/gh/bpolaszek/php-iterable-functions/branch/2.0.x-dev/graph/badge.svg [GA master]: https://github.com/bpolaszek/php-iterable-functions/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A2.0.x-dev + [GA master image]: https://github.com/bpolaszek/php-iterable-functions/workflows/Continuous%20Integration/badge.svg + +[CodeCov Master]: https://codecov.io/gh/bpolaszek/php-iterable-functions/branch/2.0.x-dev + +[Coverage image]: https://codecov.io/gh/bpolaszek/php-iterable-functions/branch/2.0.x-dev/graph/badge.svg + +[Shepherd Image]: https://shepherd.dev/github/bpolaszek/php-iterable-functions/coverage.svg + +[Shepherd Link]: https://shepherd.dev/github/bpolaszek/php-iterable-functions diff --git a/composer.json b/composer.json index b6a5021..7730355 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,8 @@ "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^0.12.67", "phpstan/phpstan-strict-rules": "^0.12.9", - "symfony/var-dumper": "^5.2" + "symfony/var-dumper": "^5.2", + "vimeo/psalm": "^4.4" }, "config": { "sort-packages": true diff --git a/psalm.xml.dist b/psalm.xml.dist new file mode 100644 index 0000000..3e6a115 --- /dev/null +++ b/psalm.xml.dist @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/iterable-functions.php b/src/iterable-functions.php index c35732c..f9a698d 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -73,9 +73,11 @@ function iterable_to_traversable(iterable $iterable): Traversable function iterable_filter(iterable $iterable, ?callable $filter = null) { if ($filter === null) { - $filter = static function ($value): bool { - return (bool) $value; - }; + $filter = + /** @param mixed $value */ + static function ($value): bool { + return (bool) $value; + }; } if ($iterable instanceof Traversable) { diff --git a/tests/IterableFilterTest.php b/tests/IterableFilterTest.php index 083d355..c1a44c5 100644 --- a/tests/IterableFilterTest.php +++ b/tests/IterableFilterTest.php @@ -23,7 +23,9 @@ it('filters an array with a callback', function (): void { $iterable = ['foo', 'bar']; - $filter = static function ($input): bool { + $filter = + /** @param mixed $input */ + static function ($input): bool { return $input === 'bar'; }; assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); @@ -31,7 +33,9 @@ it('filters a Travsersable object with a callback', function (): void { $iterable = SplFixedArray::fromArray(['foo', 'bar']); - $filter = static function ($input): bool { + $filter = + /** @param mixed $input */ + static function ($input): bool { return $input === 'bar'; }; assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); diff --git a/tests/IterableObjectTest.php b/tests/IterableObjectTest.php index f7dc96b..3eee1f5 100644 --- a/tests/IterableObjectTest.php +++ b/tests/IterableObjectTest.php @@ -17,9 +17,11 @@ $dataProvider = static function (): Generator { $data = ['foo', 'bar']; - $filter = static function ($value): bool { - return $value === 'bar'; - }; + $filter = + /** @param mixed $value */ + static function ($value): bool { + return $value === 'bar'; + }; $map = 'strtoupper'; yield from [ @@ -50,32 +52,50 @@ ]; }; -test('input: array | output: traversable', function ($data, $filter, $map, $expectedResult): void { - $iterableObject = iterable($data, $filter, $map); - assertEquals($expectedResult, iterator_to_array($iterableObject)); -})->with($dataProvider()); +test( + 'input: array | output: traversable', + /** @param array $data */ + function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { + $iterableObject = iterable($data, $filter, $map); + assertEquals($expectedResult, iterator_to_array($iterableObject)); + } +)->with($dataProvider()); -test('input: array | output: array', function ($data, $filter, $map, $expectedResult): void { - $iterableObject = iterable($data, $filter, $map); - assertEquals($expectedResult, $iterableObject->asArray()); -})->with($dataProvider()); +test( + 'input: array | output: array', + /** @param array $data */ + function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { + $iterableObject = iterable($data, $filter, $map); + assertEquals($expectedResult, $iterableObject->asArray()); + } +)->with($dataProvider()); -test('input: traversable | output: traversable', function ($data, $filter, $map, $expectedResult): void { - $data = SplFixedArray::fromArray($data); - $iterableObject = iterable($data, $filter, $map); - assertEquals($expectedResult, iterator_to_array($iterableObject)); -})->with($dataProvider()); +test( + 'input: traversable | output: traversable', + /** @param array $data */ + function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { + $data = SplFixedArray::fromArray($data); + $iterableObject = iterable($data, $filter, $map); + assertEquals($expectedResult, iterator_to_array($iterableObject)); + } +)->with($dataProvider()); -test('input: traversable | output: array', function ($data, $filter, $map, $expectedResult): void { - $data = SplFixedArray::fromArray($data); - $iterableObject = iterable($data, $filter, $map); - assertEquals($expectedResult, $iterableObject->asArray()); -})->with($dataProvider()); +test( + 'input: traversable | output: array', + /** @param array $data */ + function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { + $data = SplFixedArray::fromArray($data); + $iterableObject = iterable($data, $filter, $map); + assertEquals($expectedResult, $iterableObject->asArray()); + } +)->with($dataProvider()); it('filters the subject', function (): void { - $filter = static function ($value): bool { - return $value === 'bar'; - }; + $filter = + /** @param mixed $value */ + static function ($value): bool { + return $value === 'bar'; + }; $iterableObject = iterable(['foo', 'bar'])->filter($filter); assertEquals([1 => 'bar'], iterator_to_array($iterableObject)); }); @@ -88,9 +108,11 @@ }); it('combines filter and map', function (): void { - $filter = static function ($value): bool { - return $value === 'bar'; - }; + $filter = + /** @param mixed $value */ + static function ($value): bool { + return $value === 'bar'; + }; $map = 'strtoupper'; $iterableObject = iterable(['foo', 'bar'])->map($map)->filter($filter); assertInstanceOf(IterableObject::class, $iterableObject); diff --git a/tests/IterableReduceTest.php b/tests/IterableReduceTest.php index f6ee50d..0001cc4 100644 --- a/tests/IterableReduceTest.php +++ b/tests/IterableReduceTest.php @@ -12,16 +12,16 @@ it('reduces an array', function (): void { $iterable = [1, 2]; - $reduce = static function ($carry, $item) { - return $carry + $item; + $reduce = static function (?int $carry, int $item): int { + return (int) $carry + $item; }; assertSame(3, iterable_reduce($iterable, $reduce, 0)); }); it('reduces an traversable', function (): void { $iterable = SplFixedArray::fromArray([1, 2]); - $reduce = static function ($carry, $item) { - return $carry + $item; + $reduce = static function (?int $carry, int $item): int { + return (int) $carry + $item; }; assertSame(3, iterable_reduce($iterable, $reduce, 0)); }); From dacc89cb4e0c179870d39836a5806f992c0fb39f Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 4 Feb 2021 14:13:04 +0100 Subject: [PATCH 13/32] Feat: Add .gitattributes (#22) --- .gitattributes | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ed17d00 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text eol=lf +/.gitattributes export-ignore +/.gitignore export-ignore +/.github export-ignore +/phpcs.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/phpstan-baseline.neon export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml.dist export-ignore +/psalm-baseline.xml export-ignore +/tests export-ignore From 7da8363d9d23f10361f7c32c5fa77984cabf1976 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 4 Feb 2021 14:26:53 +0100 Subject: [PATCH 14/32] Feat: Allow object keys (#24) --- src/iterable-functions.php | 7 ++----- tests/IterableMapTest.php | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/iterable-functions.php b/src/iterable-functions.php index f9a698d..df51b9d 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -10,7 +10,6 @@ use Traversable; use function array_filter; -use function array_map; use function array_values; use function iterator_to_array; @@ -23,11 +22,9 @@ */ function iterable_map(iterable $iterable, callable $map): iterable { - if ($iterable instanceof Traversable) { - return new ArrayIterator(array_map($map, iterator_to_array($iterable))); + foreach ($iterable as $key => $item) { + yield $key => $map($item); } - - return array_map($map, $iterable); } /** diff --git a/tests/IterableMapTest.php b/tests/IterableMapTest.php index c9b7c99..86b3207 100644 --- a/tests/IterableMapTest.php +++ b/tests/IterableMapTest.php @@ -4,12 +4,17 @@ namespace BenTools\IterableFunctions\Tests; +use Generator; +use PHPUnit\Framework\Assert; use SplFixedArray; +use stdClass; use function BenTools\IterableFunctions\iterable_map; use function BenTools\IterableFunctions\iterable_to_array; use function it; use function PHPUnit\Framework\assertEquals; +use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertSame; it('maps an array', function (): void { $iterable = ['foo', 'bar']; @@ -22,3 +27,20 @@ $map = 'strtoupper'; assertEquals(['FOO', 'BAR'], iterable_to_array(iterable_map($iterable, $map))); }); + +it('maps iterable with object keys', function (): void { + foreach (iterable_map(iterableWithObjectKeys(), 'strtoupper') as $key => $item) { + assertInstanceOf(stdClass::class, $key); + assertSame('FOO', $item); + + return; + } + + Assert::fail('Did not iterate'); +}); + +/** @return Generator */ +function iterableWithObjectKeys(): Generator +{ + yield new stdClass() => 'foo'; +} From 4c46851dad46c26ac05db80ec31c083ec1688fa8 Mon Sep 17 00:00:00 2001 From: Beno!t POLASZEK Date: Thu, 11 Feb 2021 09:05:43 +0100 Subject: [PATCH 15/32] Feat: null filter (#25) --- src/IterableObject.php | 16 +++++++- src/iterable-functions.php | 4 +- tests/IterableObjectTest.php | 78 ++++++++++++++++++++++++++++++++++-- 3 files changed, 90 insertions(+), 8 deletions(-) diff --git a/src/IterableObject.php b/src/IterableObject.php index 3a07b04..4901e86 100644 --- a/src/IterableObject.php +++ b/src/IterableObject.php @@ -27,15 +27,27 @@ final class IterableObject implements IteratorAggregate /** * @param iterable|null $iterable */ - public function __construct(?iterable $iterable = null, ?callable $filter = null, ?callable $map = null) + private function __construct(?iterable $iterable = null, ?callable $filter = null, ?callable $map = null) { $this->iterable = $iterable ?? new EmptyIterator(); $this->filterFn = $filter; $this->mapFn = $map; } - public function filter(callable $filter): self + /** + * @param iterable|null $iterable + */ + public static function new(?iterable $iterable = null): self { + return new self($iterable); + } + + public function filter(?callable $filter = null): self + { + $filter = $filter ?? function ($value): bool { + return (bool) $value; + }; + return new self($this->iterable, $filter, $this->mapFn); } diff --git a/src/iterable-functions.php b/src/iterable-functions.php index df51b9d..510c335 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -111,7 +111,7 @@ function iterable_reduce(iterable $iterable, callable $reduce, $initial = null) /** * @param iterable $iterable */ -function iterable(iterable $iterable, ?callable $filter = null, ?callable $map = null): IterableObject +function iterable(iterable $iterable): IterableObject { - return new IterableObject($iterable, $filter, $map); + return IterableObject::new($iterable); } diff --git a/tests/IterableObjectTest.php b/tests/IterableObjectTest.php index 3eee1f5..c3d9377 100644 --- a/tests/IterableObjectTest.php +++ b/tests/IterableObjectTest.php @@ -8,11 +8,14 @@ use Generator; use SplFixedArray; +use function array_values; use function BenTools\IterableFunctions\iterable; +use function func_num_args; use function it; use function iterator_to_array; use function PHPUnit\Framework\assertEquals; use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertSame; use function test; $dataProvider = static function (): Generator { @@ -52,11 +55,29 @@ static function ($value): bool { ]; }; +/** + * @param iterable $iterable + */ +function create_iterable(iterable $iterable, ?callable $filter = null, ?callable $map = null): IterableObject +{ + $object = iterable($iterable); + + if ($filter !== null && func_num_args() > 1) { + $object = $object->filter($filter); + } + + if ($map !== null) { + $object = $object->map($map); + } + + return $object; +} + test( 'input: array | output: traversable', /** @param array $data */ function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { - $iterableObject = iterable($data, $filter, $map); + $iterableObject = create_iterable($data, $filter, $map); assertEquals($expectedResult, iterator_to_array($iterableObject)); } )->with($dataProvider()); @@ -65,7 +86,7 @@ function (array $data, ?callable $filter, ?callable $map, array $expectedResult) 'input: array | output: array', /** @param array $data */ function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { - $iterableObject = iterable($data, $filter, $map); + $iterableObject = create_iterable($data, $filter, $map); assertEquals($expectedResult, $iterableObject->asArray()); } )->with($dataProvider()); @@ -75,7 +96,7 @@ function (array $data, ?callable $filter, ?callable $map, array $expectedResult) /** @param array $data */ function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { $data = SplFixedArray::fromArray($data); - $iterableObject = iterable($data, $filter, $map); + $iterableObject = create_iterable($data, $filter, $map); assertEquals($expectedResult, iterator_to_array($iterableObject)); } )->with($dataProvider()); @@ -85,11 +106,32 @@ function (array $data, ?callable $filter, ?callable $map, array $expectedResult) /** @param array $data */ function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { $data = SplFixedArray::fromArray($data); - $iterableObject = iterable($data, $filter, $map); + $iterableObject = create_iterable($data, $filter, $map); assertEquals($expectedResult, $iterableObject->asArray()); } )->with($dataProvider()); +it('does not filter by default', function (): void { + $data = [ + null, + false, + true, + 0, + 1, + '0', + '1', + '', + 'foo', + ]; + + $generator = function (array $data): Generator { + yield from $data; + }; + + assertSame($data, iterable($data)->asArray()); + assertSame($data, iterable($generator($data))->asArray()); +}); + it('filters the subject', function (): void { $filter = /** @param mixed $value */ @@ -100,6 +142,34 @@ static function ($value): bool { assertEquals([1 => 'bar'], iterator_to_array($iterableObject)); }); +it('uses a truthy filter by default when filter() is invoked without arguments', function (): void { + $data = [ + null, + false, + true, + 0, + 1, + '0', + '1', + '', + 'foo', + ]; + + $truthyValues = [ + true, + 1, + '1', + 'foo', + ]; + + $generator = function (array $data): Generator { + yield from $data; + }; + + assertSame($truthyValues, array_values(iterable($data)->filter()->asArray())); + assertSame($truthyValues, array_values(iterable($generator($data))->filter()->asArray())); +}); + it('maps the subject', function (): void { $map = 'strtoupper'; $iterableObject = iterable(['foo', 'bar'])->map($map); From 3a125c1ef830fc7bb86e308df726782039b9d146 Mon Sep 17 00:00:00 2001 From: Beno!t POLASZEK Date: Thu, 11 Feb 2021 09:10:41 +0100 Subject: [PATCH 16/32] Refactor: move filter / map logic into IterableObject (#26) --- composer.json | 1 + src/IterableObject.php | 75 ++++------- src/MappedTraversable.php | 41 ++++++ src/iterable-functions.php | 33 ++--- tests/IterableFilterTest.php | 18 ++- tests/IterableMapTest.php | 11 +- tests/IterableObjectTest.php | 250 ++++++++++++----------------------- 7 files changed, 189 insertions(+), 240 deletions(-) create mode 100644 src/MappedTraversable.php diff --git a/composer.json b/composer.json index 7730355..b919f1a 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "php": "^7.3 || ^8.0" }, "require-dev": { + "bentools/cartesian-product": "^1.3", "doctrine/coding-standard": "^8.2", "pestphp/pest": "^1.0", "phpstan/extension-installer": "^1.1", diff --git a/src/IterableObject.php b/src/IterableObject.php index 4901e86..59cc296 100644 --- a/src/IterableObject.php +++ b/src/IterableObject.php @@ -4,10 +4,15 @@ namespace BenTools\IterableFunctions; -use EmptyIterator; +use CallbackFilterIterator; use IteratorAggregate; +use IteratorIterator; use Traversable; +use function array_filter; +use function array_map; +use function iterator_to_array; + /** * @internal * @@ -18,42 +23,38 @@ final class IterableObject implements IteratorAggregate /** @var iterable */ private $iterable; - /** @var callable|null */ - private $filterFn; - - /** @var callable|null */ - private $mapFn; - - /** - * @param iterable|null $iterable - */ - private function __construct(?iterable $iterable = null, ?callable $filter = null, ?callable $map = null) - { - $this->iterable = $iterable ?? new EmptyIterator(); - $this->filterFn = $filter; - $this->mapFn = $map; - } - /** - * @param iterable|null $iterable + * @param iterable $iterable */ - public static function new(?iterable $iterable = null): self + public function __construct(iterable $iterable) { - return new self($iterable); + $this->iterable = $iterable; } public function filter(?callable $filter = null): self { - $filter = $filter ?? function ($value): bool { - return (bool) $value; - }; + if ($this->iterable instanceof Traversable) { + $filter = $filter ?? + /** @param mixed $value */ + static function ($value): bool { + return (bool) $value; + }; + + return new self(new CallbackFilterIterator(new IteratorIterator($this->iterable), $filter)); + } + + $filtered = $filter === null ? array_filter($this->iterable) : array_filter($this->iterable, $filter); - return new self($this->iterable, $filter, $this->mapFn); + return new self($filtered); } - public function map(callable $map): self + public function map(callable $mapper): self { - return new self($this->iterable, $this->filterFn, $map); + if ($this->iterable instanceof Traversable) { + return new self(new MappedTraversable($this->iterable, $mapper)); + } + + return new self(array_map($mapper, $this->iterable)); } /** @@ -61,30 +62,12 @@ public function map(callable $map): self */ public function getIterator(): Traversable { - $iterable = $this->iterable; - if ($this->filterFn !== null) { - $iterable = iterable_filter($iterable, $this->filterFn); - } - - if ($this->mapFn !== null) { - $iterable = iterable_map($iterable, $this->mapFn); - } - - return iterable_to_traversable($iterable); + yield from $this->iterable; } /** @return array */ public function asArray(): array { - $iterable = $this->iterable; - if ($this->filterFn !== null) { - $iterable = iterable_filter($iterable, $this->filterFn); - } - - if ($this->mapFn !== null) { - $iterable = iterable_map($iterable, $this->mapFn); - } - - return iterable_to_array($iterable); + return $this->iterable instanceof Traversable ? iterator_to_array($this->iterable) : $this->iterable; } } diff --git a/src/MappedTraversable.php b/src/MappedTraversable.php new file mode 100644 index 0000000..d992c76 --- /dev/null +++ b/src/MappedTraversable.php @@ -0,0 +1,41 @@ + + */ +final class MappedTraversable implements IteratorAggregate +{ + /** @var Traversable */ + private $traversable; + + /** @var callable */ + private $mapper; + + /** + * @param Traversable $traversable + */ + public function __construct(Traversable $traversable, callable $mapper) + { + $this->traversable = $traversable; + $this->mapper = $mapper; + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + foreach ($this->traversable as $key => $value) { + yield $key => ($this->mapper)($value); + } + } +} diff --git a/src/iterable-functions.php b/src/iterable-functions.php index 510c335..1fde255 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -5,12 +5,11 @@ namespace BenTools\IterableFunctions; use ArrayIterator; -use CallbackFilterIterator; -use IteratorIterator; +use EmptyIterator; use Traversable; -use function array_filter; use function array_values; +use function is_array; use function iterator_to_array; /** @@ -22,9 +21,9 @@ */ function iterable_map(iterable $iterable, callable $map): iterable { - foreach ($iterable as $key => $item) { - yield $key => $map($item); - } + $mapped = iterable($iterable)->map($map); + + return is_array($iterable) ? $mapped->asArray() : $mapped; } /** @@ -65,23 +64,13 @@ function iterable_to_traversable(iterable $iterable): Traversable * * @param iterable $iterable * - * @return array|CallbackFilterIterator + * @return iterable */ -function iterable_filter(iterable $iterable, ?callable $filter = null) +function iterable_filter(iterable $iterable, ?callable $filter = null): iterable { - if ($filter === null) { - $filter = - /** @param mixed $value */ - static function ($value): bool { - return (bool) $value; - }; - } - - if ($iterable instanceof Traversable) { - return new CallbackFilterIterator(new IteratorIterator($iterable), $filter); - } + $filtered = iterable($iterable)->filter($filter); - return array_filter($iterable, $filter); + return is_array($iterable) ? $filtered->asArray() : $filtered; } /** @@ -111,7 +100,7 @@ function iterable_reduce(iterable $iterable, callable $reduce, $initial = null) /** * @param iterable $iterable */ -function iterable(iterable $iterable): IterableObject +function iterable(?iterable $iterable): IterableObject { - return IterableObject::new($iterable); + return new IterableObject($iterable ?? new EmptyIterator()); } diff --git a/tests/IterableFilterTest.php b/tests/IterableFilterTest.php index c1a44c5..ae8e034 100644 --- a/tests/IterableFilterTest.php +++ b/tests/IterableFilterTest.php @@ -5,20 +5,24 @@ namespace BenTools\IterableFunctions\Tests; use SplFixedArray; +use Traversable; +use function assert; use function BenTools\IterableFunctions\iterable_filter; -use function BenTools\IterableFunctions\iterable_to_array; use function it; -use function PHPUnit\Framework\assertEquals; +use function iterator_to_array; +use function PHPUnit\Framework\assertSame; it('filters an array', function (): void { $iterable = [false, true]; - assertEquals([1 => true], iterable_to_array(iterable_filter($iterable))); + assertSame([1 => true], iterable_filter($iterable)); }); it('filters a Traversable object', function (): void { $iterable = SplFixedArray::fromArray([false, true]); - assertEquals([1 => true], iterable_to_array(iterable_filter($iterable))); + $filtered = iterable_filter($iterable); + assert($filtered instanceof Traversable); + assertSame([1 => true], iterator_to_array($filtered)); }); it('filters an array with a callback', function (): void { @@ -28,7 +32,7 @@ static function ($input): bool { return $input === 'bar'; }; - assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); + assertSame([1 => 'bar'], iterable_filter($iterable, $filter)); }); it('filters a Travsersable object with a callback', function (): void { @@ -38,5 +42,7 @@ static function ($input): bool { static function ($input): bool { return $input === 'bar'; }; - assertEquals([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); + $filtered = iterable_filter($iterable, $filter); + assert($filtered instanceof Traversable); + assertSame([1 => 'bar'], iterator_to_array($filtered)); }); diff --git a/tests/IterableMapTest.php b/tests/IterableMapTest.php index 86b3207..c7178f3 100644 --- a/tests/IterableMapTest.php +++ b/tests/IterableMapTest.php @@ -8,24 +8,27 @@ use PHPUnit\Framework\Assert; use SplFixedArray; use stdClass; +use Traversable; +use function assert; use function BenTools\IterableFunctions\iterable_map; -use function BenTools\IterableFunctions\iterable_to_array; use function it; -use function PHPUnit\Framework\assertEquals; +use function iterator_to_array; use function PHPUnit\Framework\assertInstanceOf; use function PHPUnit\Framework\assertSame; it('maps an array', function (): void { $iterable = ['foo', 'bar']; $map = 'strtoupper'; - assertEquals(['FOO', 'BAR'], iterable_to_array(iterable_map($iterable, $map))); + assertSame(['FOO', 'BAR'], iterable_map($iterable, $map)); }); it('maps a Traversable object', function (): void { $iterable = SplFixedArray::fromArray(['foo', 'bar']); $map = 'strtoupper'; - assertEquals(['FOO', 'BAR'], iterable_to_array(iterable_map($iterable, $map))); + $mapped = iterable_map($iterable, $map); + assert($mapped instanceof Traversable); + assertSame(['FOO', 'BAR'], iterator_to_array($mapped)); }); it('maps iterable with object keys', function (): void { diff --git a/tests/IterableObjectTest.php b/tests/IterableObjectTest.php index c3d9377..10fde4c 100644 --- a/tests/IterableObjectTest.php +++ b/tests/IterableObjectTest.php @@ -6,188 +6,114 @@ use BenTools\IterableFunctions\IterableObject; use Generator; -use SplFixedArray; +use IteratorAggregate; +use Traversable; use function array_values; +use function BenTools\CartesianProduct\cartesian_product; use function BenTools\IterableFunctions\iterable; -use function func_num_args; +use function is_callable; use function it; -use function iterator_to_array; use function PHPUnit\Framework\assertEquals; use function PHPUnit\Framework\assertInstanceOf; use function PHPUnit\Framework\assertSame; -use function test; - -$dataProvider = static function (): Generator { - $data = ['foo', 'bar']; - $filter = - /** @param mixed $value */ - static function ($value): bool { - return $value === 'bar'; - }; - $map = 'strtoupper'; - - yield from [ - [ - $data, - null, - null, - ['foo', 'bar'], - ], - [ - $data, - $filter, - null, - [1 => 'bar'], - ], - [ - $data, - null, - $map, - ['FOO', 'BAR'], - ], - [ - $data, - $filter, - $map, - [1 => 'BAR'], - ], - ]; -}; - -/** - * @param iterable $iterable - */ -function create_iterable(iterable $iterable, ?callable $filter = null, ?callable $map = null): IterableObject -{ - $object = iterable($iterable); - - if ($filter !== null && func_num_args() > 1) { - $object = $object->filter($filter); - } - - if ($map !== null) { - $object = $object->map($map); - } - - return $object; -} - -test( - 'input: array | output: traversable', - /** @param array $data */ - function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { - $iterableObject = create_iterable($data, $filter, $map); - assertEquals($expectedResult, iterator_to_array($iterableObject)); - } -)->with($dataProvider()); - -test( - 'input: array | output: array', - /** @param array $data */ - function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { - $iterableObject = create_iterable($data, $filter, $map); - assertEquals($expectedResult, $iterableObject->asArray()); - } -)->with($dataProvider()); - -test( - 'input: traversable | output: traversable', - /** @param array $data */ - function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { - $data = SplFixedArray::fromArray($data); - $iterableObject = create_iterable($data, $filter, $map); - assertEquals($expectedResult, iterator_to_array($iterableObject)); - } -)->with($dataProvider()); - -test( - 'input: traversable | output: array', - /** @param array $data */ - function (array $data, ?callable $filter, ?callable $map, array $expectedResult): void { - $data = SplFixedArray::fromArray($data); - $iterableObject = create_iterable($data, $filter, $map); - assertEquals($expectedResult, $iterableObject->asArray()); - } -)->with($dataProvider()); +use function strtolower; +use function strtoupper; -it('does not filter by default', function (): void { - $data = [ +$combinations = cartesian_product([ + 'input' => [ + null, + ['', 'foo', 'bar'], + new class implements IteratorAggregate { + /** @return Traversable */ + public function getIterator(): Traversable + { + yield ''; + yield 'foo'; + yield 'bar'; + } + }, + ], + 'mapper' => [ null, + static function (): callable { + return static function (string $value): string { + return strtoupper($value); + }; + }, + ], + 'filtered' => [ false, true, - 0, - 1, - '0', - '1', - '', - 'foo', - ]; + ], + 'filter' => [ + null, + static function (): callable { + return static function (string $value): bool { + return strtolower($value) === 'bar'; + }; + }, + ], +]); - $generator = function (array $data): Generator { - yield from $data; - }; - assertSame($data, iterable($data)->asArray()); - assertSame($data, iterable($generator($data))->asArray()); -}); +it( + 'produces the expected result', + function (?iterable $input, ?callable $mapper, bool $filtered, ?callable $filter): void { + $iterable = iterable($input); -it('filters the subject', function (): void { - $filter = - /** @param mixed $value */ - static function ($value): bool { - return $value === 'bar'; - }; - $iterableObject = iterable(['foo', 'bar'])->filter($filter); - assertEquals([1 => 'bar'], iterator_to_array($iterableObject)); -}); + if ($input === null) { + assertSame([], $iterable->asArray()); -it('uses a truthy filter by default when filter() is invoked without arguments', function (): void { - $data = [ - null, - false, - true, - 0, - 1, - '0', - '1', - '', - 'foo', - ]; + return; + } - $truthyValues = [ - true, - 1, - '1', - 'foo', - ]; + // Default expectation + $expected = ['', 'foo', 'bar']; - $generator = function (array $data): Generator { - yield from $data; - }; + // Expectation when iterable is mapped + if ($mapper !== null) { + $iterable = $iterable->map($mapper); + $expected = ['', 'FOO', 'BAR']; + } - assertSame($truthyValues, array_values(iterable($data)->filter()->asArray())); - assertSame($truthyValues, array_values(iterable($generator($data))->filter()->asArray())); -}); + // Expectation when iterable is filtered + if ($filtered === true) { + $iterable = $iterable->filter($filter); -it('maps the subject', function (): void { - $map = 'strtoupper'; - $iterableObject = iterable(['foo', 'bar'])->map($map); - assertInstanceOf(IterableObject::class, $iterableObject); - assertEquals(['FOO', 'BAR'], iterator_to_array($iterableObject)); -}); + // empty string should be removed when iterable is filtered without callable + unset($expected[0]); + + // empty string and "foo" should be removed otherwise + if (is_callable($filter)) { + unset($expected[1]); + } + } + + assertSame($expected, $iterable->asArray()); + } +)->with($combinations); -it('combines filter and map', function (): void { - $filter = - /** @param mixed $value */ - static function ($value): bool { - return $value === 'bar'; +it('can filter first, then map', function (iterable $input): void { + $map = + /** @return mixed */ + static function (string $value) { + $map = ['zero' => 0, 'one' => 1, 'two' => 2]; + + return $map[$value]; }; - $map = 'strtoupper'; - $iterableObject = iterable(['foo', 'bar'])->map($map)->filter($filter); - assertInstanceOf(IterableObject::class, $iterableObject); - assertEquals([1 => 'BAR'], iterator_to_array($iterableObject)); - $iterableObject = iterable(['foo', 'bar'])->filter($filter)->map($map); + $input = ['zero', 'one', 'two']; + + $iterableObject = iterable($input)->filter()->map($map); assertInstanceOf(IterableObject::class, $iterableObject); - assertEquals([1 => 'BAR'], iterator_to_array($iterableObject)); + assertEquals([0, 1, 2], array_values($iterableObject->asArray())); +})->with(function (): Generator { + $input = ['zero', 'one', 'two']; + yield [$input]; + yield [ + /** @return Generator */ + (static function (array $input): Generator { + yield from $input; + })($input), + ]; }); From 8d61a26fbc9ebb4c830189abab82993293f6d8b9 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 11 Feb 2021 09:12:31 +0100 Subject: [PATCH 17/32] Chore: Add types to iterable_to_array() (#27) --- src/iterable-functions.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/iterable-functions.php b/src/iterable-functions.php index 1fde255..d891839 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -27,12 +27,17 @@ function iterable_map(iterable $iterable, callable $map): iterable } /** - * Copy the iterable into an array. If the iterable is already an array, return it. + * Copy the iterable into an array. * - * @param iterable $iterable + * @param iterable $iterable * @param bool $preserveKeys [optional] Whether to use the iterator element keys as index. * - * @return array + * @return array + * + * @psalm-return ($preserveKeys is true ? array : array) + * @psalm-template TKey as array-key + * @phpstan-template TKey + * @template TValue */ function iterable_to_array(iterable $iterable, bool $preserveKeys = true): array { From 7b3e19b17657ed50e70b9bf16bdcb01329dc5e66 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 11 Feb 2021 09:13:33 +0100 Subject: [PATCH 18/32] Chore: Add types to iterable_to_traversable() (#29) --- src/iterable-functions.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/iterable-functions.php b/src/iterable-functions.php index d891839..cd30963 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -51,9 +51,12 @@ function iterable_to_array(iterable $iterable, bool $preserveKeys = true): array /** * If the iterable is not instance of Traversable, it is an array => convert it to an ArrayIterator. * - * @param iterable $iterable + * @param iterable $iterable + * + * @return Traversable * - * @return Traversable + * @template TKey + * @template TValue */ function iterable_to_traversable(iterable $iterable): Traversable { From 5d9968228a450b07b9b630aee1b8443726d4e11f Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 11 Feb 2021 09:14:01 +0100 Subject: [PATCH 19/32] Chore: Add types to iterable_reduce() (#31) --- src/iterable-functions.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/iterable-functions.php b/src/iterable-functions.php index cd30963..0b03386 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -84,17 +84,16 @@ function iterable_filter(iterable $iterable, ?callable $filter = null): iterable /** * Reduces an iterable. * - * @param iterable $iterable - * @param callable(mixed, mixed):mixed $reduce + * @param iterable $iterable + * @param callable(TResult|null, TValue):TResult $reduce + * @param TResult|null $initial * - * @return mixed + * @return TResult|null * - * @psalm-template TValue + * @template TValue * @template TResult - * @psalm-param iterable $iterable - * @psalm-param callable(TResult|null, TValue):TResult $reduce - * @psalm-param TResult|null $initial - * @psalm-return TResult|null + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint */ function iterable_reduce(iterable $iterable, callable $reduce, $initial = null) { From 09aeff9960ce8b9b1c9b7d87c84f5a18e5ea5fae Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 11 Feb 2021 09:33:48 +0100 Subject: [PATCH 20/32] Chore: Add types to iterable_map() (#28) --- src/iterable-functions.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/iterable-functions.php b/src/iterable-functions.php index 0b03386..a677af6 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -15,13 +15,18 @@ /** * Maps a callable to an iterable. * - * @param iterable $iterable + * @param iterable $iterable + * @param callable(TValue):TResult $mapper * - * @return iterable + * @return iterable + * + * @template TKey + * @template TValue + * @template TResult */ -function iterable_map(iterable $iterable, callable $map): iterable +function iterable_map(iterable $iterable, callable $mapper): iterable { - $mapped = iterable($iterable)->map($map); + $mapped = iterable($iterable)->map($mapper); return is_array($iterable) ? $mapped->asArray() : $mapped; } From a11bac0c963c19938e815d187d5808c44ca55ecf Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 11 Feb 2021 09:34:14 +0100 Subject: [PATCH 21/32] Chore: Add types to iterable_filter() (#30) --- src/iterable-functions.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/iterable-functions.php b/src/iterable-functions.php index a677af6..e244236 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -75,9 +75,14 @@ function iterable_to_traversable(iterable $iterable): Traversable /** * Filters an iterable. * - * @param iterable $iterable + * @param (callable(TValue):bool)|null $filter * - * @return iterable + * @psalm-param iterable $iterable + * @phpstan-param iterable $iterable https://github.com/phpstan/phpstan/issues/4498 + * @psalm-return iterable + * @phpstan-return iterable https://github.com/phpstan/phpstan/issues/4498 + * @template TKey + * @template TValue */ function iterable_filter(iterable $iterable, ?callable $filter = null): iterable { From d0e80ba02ba3513e13377ab8f77cbd152b201a53 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Mon, 22 Feb 2021 07:01:07 +0100 Subject: [PATCH 22/32] Chore: Add types to iterable() and IterableObject (#23) --- phpstan.neon.dist | 7 +++++++ psalm-baseline.xml | 13 +++++++++++++ psalm.xml.dist | 1 + src/IterableObject.php | 29 ++++++++++++++++++++--------- src/iterable-functions.php | 7 ++++++- 5 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 psalm-baseline.xml diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 8ea502f..7e29c96 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -3,3 +3,10 @@ parameters: paths: - %currentWorkingDirectory%/src - %currentWorkingDirectory%/tests + + ignoreErrors: + # https://github.com/phpstan/phpstan/issues/4498 + - '~Method BenTools\\IterableFunctions\\IterableObject::filter\(\) should return BenTools\\IterableFunctions\\IterableObject but returns BenTools\\IterableFunctions\\IterableObject~' + - '~Method BenTools\\IterableFunctions\\IterableObject::map\(\) should return BenTools\\IterableFunctions\\IterableObject but returns BenTools\\IterableFunctions\\IterableObject~' + + - '~Function BenTools\\IterableFunctions\\iterable_map\(\) should return iterable but returns array\|BenTools\\IterableFunctions\\IterableObject~' diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..555826d --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,13 @@ + + + + + is_array($iterable) ? $filtered->asArray() : $filtered + is_array($iterable) ? $mapped->asArray() : $mapped + + + iterable<TKey, TResult> + iterable<TKey, TValue> + + + diff --git a/psalm.xml.dist b/psalm.xml.dist index 3e6a115..02ceeb4 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -3,6 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" + errorBaseline="psalm-baseline.xml" > diff --git a/src/IterableObject.php b/src/IterableObject.php index 59cc296..b0e4796 100644 --- a/src/IterableObject.php +++ b/src/IterableObject.php @@ -16,21 +16,27 @@ /** * @internal * - * @implements IteratorAggregate + * @template TKey + * @template TValue + * + * @implements IteratorAggregate */ final class IterableObject implements IteratorAggregate { - /** @var iterable */ + /** @var iterable */ private $iterable; - /** - * @param iterable $iterable - */ + /** @param iterable $iterable */ public function __construct(iterable $iterable) { $this->iterable = $iterable; } + /** + * @param (callable(TValue):bool)|null $filter + * + * @return self + */ public function filter(?callable $filter = null): self { if ($this->iterable instanceof Traversable) { @@ -48,6 +54,13 @@ static function ($value): bool { return new self($filtered); } + /** + * @param callable(TValue):TResult $mapper + * + * @return self + * + * @template TResult + */ public function map(callable $mapper): self { if ($this->iterable instanceof Traversable) { @@ -57,15 +70,13 @@ public function map(callable $mapper): self return new self(array_map($mapper, $this->iterable)); } - /** - * @return Traversable - */ + /** @return Traversable */ public function getIterator(): Traversable { yield from $this->iterable; } - /** @return array */ + /** @return array */ public function asArray(): array { return $this->iterable instanceof Traversable ? iterator_to_array($this->iterable) : $this->iterable; diff --git a/src/iterable-functions.php b/src/iterable-functions.php index e244236..f7f379c 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -115,7 +115,12 @@ function iterable_reduce(iterable $iterable, callable $reduce, $initial = null) } /** - * @param iterable $iterable + * @param iterable|null $iterable + * + * @return IterableObject + * + * @template TKey + * @template TValue */ function iterable(?iterable $iterable): IterableObject { From f975d69023a3774f2a142e80ce8892938473749c Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 25 Feb 2021 09:04:18 +0100 Subject: [PATCH 23/32] Fix: test values being overwritten (#34) --- tests/IterableObjectTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/IterableObjectTest.php b/tests/IterableObjectTest.php index 10fde4c..be83845 100644 --- a/tests/IterableObjectTest.php +++ b/tests/IterableObjectTest.php @@ -102,7 +102,6 @@ static function (string $value) { return $map[$value]; }; - $input = ['zero', 'one', 'two']; $iterableObject = iterable($input)->filter()->map($map); assertInstanceOf(IterableObject::class, $iterableObject); From a002e1fc2c5a5502e7e39487b3ec4fc0ef5240fb Mon Sep 17 00:00:00 2001 From: Beno!t POLASZEK Date: Thu, 25 Feb 2021 09:38:14 +0100 Subject: [PATCH 24/32] docs: improve introduction --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b91fdd..c9cfa22 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,13 @@ Iterable functions ================== -Provides additional functions to work with [iterable](https://wiki.php.net/rfc/iterable) variables (even on PHP5.3+). +This package provides functions to work with [iterables](https://wiki.php.net/rfc/iterable), as you usually do with arrays: + +- [iterable_to_array()](#iterable_to_array) +- [iterable_to_traversable()](#iterable_to_traversable) +- [iterable_map()](#iterable_map) +- [iterable_reduce()](#iterable_reduce) +- [iterable_filter()](#iterable_filter) iterable_to_array() ------------------- From 70b4a5429f1e843ecc8827c893a1bfcfdfc21081 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 25 Feb 2021 12:07:30 +0100 Subject: [PATCH 25/32] Feat: iterable_values() (#33) --- README.md | 19 ++++++++++++++ src/IterableObject.php | 8 ++++++ src/WithoutKeysTraversable.php | 39 +++++++++++++++++++++++++++ src/iterable-functions.php | 16 ++++++++++++ tests/IterableObjectTest.php | 8 ++++++ tests/IterableValuesTest.php | 48 ++++++++++++++++++++++++++++++++++ 6 files changed, 138 insertions(+) create mode 100644 src/WithoutKeysTraversable.php create mode 100644 tests/IterableValuesTest.php diff --git a/README.md b/README.md index c9cfa22..26e8903 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,25 @@ foreach (iterable_filter($generator(), $filter) as $item) { } ``` +iterable_values() +-------------- + +Works like an `array_values` with an `array` or a `Traversable`. + +```php +use function BenTools\IterableFunctions\iterable_values; + +$generator = function () { + yield 'a' => 'a'; + yield 'b' => 'b'; +}; + +foreach (iterable_values($generator()) as $key => $value) { + var_dump($key); // 0, 1 + var_dump($value); // a, b +} +``` + Iterable fluent interface ========================= diff --git a/src/IterableObject.php b/src/IterableObject.php index b0e4796..5be9f18 100644 --- a/src/IterableObject.php +++ b/src/IterableObject.php @@ -70,6 +70,14 @@ public function map(callable $mapper): self return new self(array_map($mapper, $this->iterable)); } + /** + * @return self + */ + public function values(): self + { + return new self(new WithoutKeysTraversable($this->iterable)); + } + /** @return Traversable */ public function getIterator(): Traversable { diff --git a/src/WithoutKeysTraversable.php b/src/WithoutKeysTraversable.php new file mode 100644 index 0000000..6a97776 --- /dev/null +++ b/src/WithoutKeysTraversable.php @@ -0,0 +1,39 @@ + + */ +final class WithoutKeysTraversable implements IteratorAggregate +{ + /** @var iterable */ + private $iterable; + + /** + * @param iterable $iterable + */ + public function __construct(iterable $iterable) + { + $this->iterable = $iterable; + } + + /** + * @return Generator + */ + public function getIterator(): Generator + { + foreach ($this->iterable as $value) { + yield $value; + } + } +} diff --git a/src/iterable-functions.php b/src/iterable-functions.php index f7f379c..c9cf941 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -114,6 +114,22 @@ function iterable_reduce(iterable $iterable, callable $reduce, $initial = null) return $initial; } +/** + * Yields iterable values (leaving out keys). + * + * @param iterable $iterable + * + * @return iterable + * + * @template TValue + */ +function iterable_values(iterable $iterable): iterable +{ + $withoutKeys = iterable($iterable)->values(); + + return is_array($iterable) ? $withoutKeys->asArray() : $withoutKeys; +} + /** * @param iterable|null $iterable * diff --git a/tests/IterableObjectTest.php b/tests/IterableObjectTest.php index be83845..ded2efe 100644 --- a/tests/IterableObjectTest.php +++ b/tests/IterableObjectTest.php @@ -116,3 +116,11 @@ static function (string $value) { })($input), ]; }); + +it('strips key through values()', function (): void { + $input = ['x' => 'zero', 'y' => 'one', 'z' => 'two']; + + $iterableObject = iterable($input)->values(); + assertInstanceOf(IterableObject::class, $iterableObject); + assertEquals(['zero', 'one', 'two'], $iterableObject->asArray()); +}); diff --git a/tests/IterableValuesTest.php b/tests/IterableValuesTest.php new file mode 100644 index 0000000..4a0c55b --- /dev/null +++ b/tests/IterableValuesTest.php @@ -0,0 +1,48 @@ + true]; + + // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedForeach + foreach (iterable_values($iterable) as $key => $value) { + } + + if (! isset($key, $value)) { + Assert::fail('No values were returned'); + } + + assertSame(0, $key); + assertSame(true, $value); + } +); + +it( + 'gets values of Traversable object', + function (): void { + $iterable = new ArrayIterator(['b' => true]); + + // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedForeach + foreach (iterable_values($iterable) as $key => $value) { + } + + if (! isset($key, $value)) { + Assert::fail('No values were returned'); + } + + assertSame(0, $key); + assertSame(true, $value); + } +); From 9edc1b89c323b74c152d89ba6faa22c4f40129f6 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 25 Feb 2021 12:08:30 +0100 Subject: [PATCH 26/32] Fix: Improve reduce() signature to exclude null from return type when non-null initial value is passed (#35) --- phpstan-baseline.neon | 12 ++++++++++++ phpstan.neon.dist | 3 +++ src/iterable-functions.php | 6 +++--- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 phpstan-baseline.neon diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..4b9d758 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,12 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$ of callable callable\\(TResult, TValue\\)\\: TResult expects TResult, TResult\\|null given\\.$#" + count: 1 + path: src/iterable-functions.php + + - + message: "#^Function BenTools\\\\IterableFunctions\\\\iterable_reduce\\(\\) should return TResult but returns TResult\\|null\\.$#" + count: 1 + path: src/iterable-functions.php + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 7e29c96..aee4ee1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -10,3 +10,6 @@ parameters: - '~Method BenTools\\IterableFunctions\\IterableObject::map\(\) should return BenTools\\IterableFunctions\\IterableObject but returns BenTools\\IterableFunctions\\IterableObject~' - '~Function BenTools\\IterableFunctions\\iterable_map\(\) should return iterable but returns array\|BenTools\\IterableFunctions\\IterableObject~' + +includes: + - phpstan-baseline.neon diff --git a/src/iterable-functions.php b/src/iterable-functions.php index c9cf941..2472ef4 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -95,10 +95,10 @@ function iterable_filter(iterable $iterable, ?callable $filter = null): iterable * Reduces an iterable. * * @param iterable $iterable - * @param callable(TResult|null, TValue):TResult $reduce - * @param TResult|null $initial + * @param TResult $initial + * @param callable(TResult, TValue):TResult $reduce * - * @return TResult|null + * @return TResult * * @template TValue * @template TResult From 007c48387b757ca3e93c13a3c22c0eae3b5eaaf2 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Sun, 28 Feb 2021 10:45:57 +0100 Subject: [PATCH 27/32] Docs: Add missing iterable_values() into summary (#36) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 26e8903..65e7015 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This package provides functions to work with [iterables](https://wiki.php.net/rf - [iterable_map()](#iterable_map) - [iterable_reduce()](#iterable_reduce) - [iterable_filter()](#iterable_filter) +- [iterable_values()](#iterable_values) iterable_to_array() ------------------- From 9a72fed9c6e867bb7a79e6a1d8b79601a2ff1a28 Mon Sep 17 00:00:00 2001 From: Beno!t POLASZEK Date: Thu, 11 Mar 2021 09:40:50 +0100 Subject: [PATCH 28/32] docs: improve installation --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 65e7015..d1a5801 100644 --- a/README.md +++ b/README.md @@ -190,17 +190,12 @@ $iterable->asArray(); // array ['banana', 'pineapple'] Installation ============ -With composer (they'll be autoloaded): - ``` -composer require bentools/iterable-functions +composer require bentools/iterable-functions:^2.0 ``` -Or manually: +For PHP5+ compatibility, check out the [1.x branch](https://github.com/bpolaszek/php-iterable-functions/tree/1.x). -```php -require_once '/path/to/this/library/src/iterable-functions.php'; -``` Unit tests ========== From 64a0bcde04443ea5c3c37ec95b69b405877a5100 Mon Sep 17 00:00:00 2001 From: Beno!t POLASZEK Date: Sun, 25 Jul 2021 08:10:11 +0200 Subject: [PATCH 29/32] fix: remove PHPStan ignored errors as phpstan/phpstan#4498 is resolved --- phpstan-baseline.neon | 12 ------------ phpstan.neon.dist | 9 --------- 2 files changed, 21 deletions(-) delete mode 100644 phpstan-baseline.neon diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index 4b9d758..0000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,12 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Parameter \\#1 \\$ of callable callable\\(TResult, TValue\\)\\: TResult expects TResult, TResult\\|null given\\.$#" - count: 1 - path: src/iterable-functions.php - - - - message: "#^Function BenTools\\\\IterableFunctions\\\\iterable_reduce\\(\\) should return TResult but returns TResult\\|null\\.$#" - count: 1 - path: src/iterable-functions.php - diff --git a/phpstan.neon.dist b/phpstan.neon.dist index aee4ee1..1e018d5 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -4,12 +4,3 @@ parameters: - %currentWorkingDirectory%/src - %currentWorkingDirectory%/tests - ignoreErrors: - # https://github.com/phpstan/phpstan/issues/4498 - - '~Method BenTools\\IterableFunctions\\IterableObject::filter\(\) should return BenTools\\IterableFunctions\\IterableObject but returns BenTools\\IterableFunctions\\IterableObject~' - - '~Method BenTools\\IterableFunctions\\IterableObject::map\(\) should return BenTools\\IterableFunctions\\IterableObject but returns BenTools\\IterableFunctions\\IterableObject~' - - - '~Function BenTools\\IterableFunctions\\iterable_map\(\) should return iterable but returns array\|BenTools\\IterableFunctions\\IterableObject~' - -includes: - - phpstan-baseline.neon From 60d35f06f149f5ce3fabb71025cb7d4ec971c219 Mon Sep 17 00:00:00 2001 From: Beno!t POLASZEK Date: Sun, 25 Jul 2021 08:14:02 +0200 Subject: [PATCH 30/32] style: ignore rule that doesn't apply on PHP < 7.4 --- phpcs.xml.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 82c4a0e..ca1e5ad 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -18,6 +18,7 @@ + src/ From 318fa883d575b4421e404b3fe680d11ee4333ce5 Mon Sep 17 00:00:00 2001 From: Beno!t POLASZEK Date: Sun, 25 Jul 2021 08:14:10 +0200 Subject: [PATCH 31/32] feat: do not return same type as input iterable anymore in iterable_map() and iterable_filter() --- src/iterable-functions.php | 13 +++---------- tests/IterableFilterTest.php | 5 +++-- tests/IterableMapTest.php | 6 +++--- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/iterable-functions.php b/src/iterable-functions.php index 2472ef4..981332e 100644 --- a/src/iterable-functions.php +++ b/src/iterable-functions.php @@ -9,7 +9,6 @@ use Traversable; use function array_values; -use function is_array; use function iterator_to_array; /** @@ -26,9 +25,7 @@ */ function iterable_map(iterable $iterable, callable $mapper): iterable { - $mapped = iterable($iterable)->map($mapper); - - return is_array($iterable) ? $mapped->asArray() : $mapped; + return iterable($iterable)->map($mapper); } /** @@ -86,9 +83,7 @@ function iterable_to_traversable(iterable $iterable): Traversable */ function iterable_filter(iterable $iterable, ?callable $filter = null): iterable { - $filtered = iterable($iterable)->filter($filter); - - return is_array($iterable) ? $filtered->asArray() : $filtered; + return iterable($iterable)->filter($filter); } /** @@ -125,9 +120,7 @@ function iterable_reduce(iterable $iterable, callable $reduce, $initial = null) */ function iterable_values(iterable $iterable): iterable { - $withoutKeys = iterable($iterable)->values(); - - return is_array($iterable) ? $withoutKeys->asArray() : $withoutKeys; + return iterable($iterable)->values(); } /** diff --git a/tests/IterableFilterTest.php b/tests/IterableFilterTest.php index ae8e034..ea0b4ca 100644 --- a/tests/IterableFilterTest.php +++ b/tests/IterableFilterTest.php @@ -9,13 +9,14 @@ use function assert; use function BenTools\IterableFunctions\iterable_filter; +use function BenTools\IterableFunctions\iterable_to_array; use function it; use function iterator_to_array; use function PHPUnit\Framework\assertSame; it('filters an array', function (): void { $iterable = [false, true]; - assertSame([1 => true], iterable_filter($iterable)); + assertSame([1 => true], iterable_to_array(iterable_filter($iterable))); }); it('filters a Traversable object', function (): void { @@ -32,7 +33,7 @@ static function ($input): bool { return $input === 'bar'; }; - assertSame([1 => 'bar'], iterable_filter($iterable, $filter)); + assertSame([1 => 'bar'], iterable_to_array(iterable_filter($iterable, $filter))); }); it('filters a Travsersable object with a callback', function (): void { diff --git a/tests/IterableMapTest.php b/tests/IterableMapTest.php index c7178f3..618a164 100644 --- a/tests/IterableMapTest.php +++ b/tests/IterableMapTest.php @@ -12,15 +12,15 @@ use function assert; use function BenTools\IterableFunctions\iterable_map; +use function BenTools\IterableFunctions\iterable_to_array; use function it; -use function iterator_to_array; use function PHPUnit\Framework\assertInstanceOf; use function PHPUnit\Framework\assertSame; it('maps an array', function (): void { $iterable = ['foo', 'bar']; $map = 'strtoupper'; - assertSame(['FOO', 'BAR'], iterable_map($iterable, $map)); + assertSame(['FOO', 'BAR'], iterable_to_array(iterable_map($iterable, $map))); }); it('maps a Traversable object', function (): void { @@ -28,7 +28,7 @@ $map = 'strtoupper'; $mapped = iterable_map($iterable, $map); assert($mapped instanceof Traversable); - assertSame(['FOO', 'BAR'], iterator_to_array($mapped)); + assertSame(['FOO', 'BAR'], iterable_to_array($mapped)); }); it('maps iterable with object keys', function (): void { From 55567d2d7285f60f32e401ab921e7b27fa70c5e9 Mon Sep 17 00:00:00 2001 From: Beno!t POLASZEK Date: Mon, 26 Jul 2021 09:49:08 +0200 Subject: [PATCH 32/32] build: drop PHP 7.3 support --- .github/workflows/coding-standards.yml | 2 +- .github/workflows/continuous-integration.yml | 3 +-- .github/workflows/static-analysis.yml | 4 ++-- composer.json | 2 +- phpcs.xml.dist | 1 - src/IterableObject.php | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 6007fbe..2582648 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: php-version: - - "7.3" + - "7.4" steps: - name: "Checkout" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 2e5ad90..9e4bf6c 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -14,14 +14,13 @@ jobs: strategy: matrix: php-version: - - "7.3" - "7.4" - "8.0" dependencies: - "highest" include: - dependencies: "lowest" - php-version: "7.3" + php-version: "7.4" steps: - name: "Checkout" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index f673d86..23cd0e1 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: php-version: - - "7.3" + - "7.4" steps: - name: "Checkout code" @@ -40,7 +40,7 @@ jobs: strategy: matrix: php-version: - - "7.3" + - "7.4" steps: - name: Checkout code diff --git a/composer.json b/composer.json index b919f1a..b04bdf7 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ ] }, "require": { - "php": "^7.3 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "bentools/cartesian-product": "^1.3", diff --git a/phpcs.xml.dist b/phpcs.xml.dist index ca1e5ad..82c4a0e 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -18,7 +18,6 @@ - src/ diff --git a/src/IterableObject.php b/src/IterableObject.php index 5be9f18..c3526f1 100644 --- a/src/IterableObject.php +++ b/src/IterableObject.php @@ -40,7 +40,7 @@ public function __construct(iterable $iterable) public function filter(?callable $filter = null): self { if ($this->iterable instanceof Traversable) { - $filter = $filter ?? + $filter ??= /** @param mixed $value */ static function ($value): bool { return (bool) $value;