├── .gitignore ├── tests ├── bootstrap.php └── unit │ ├── operations │ ├── FlatMapOperationTest.php │ ├── MappingOperationTest.php │ ├── FilterOperationTest.php │ ├── LimitOperationTest.php │ ├── SkipOperationTest.php │ ├── DistinctOperationTest.php │ └── SortedOperationTest.php │ ├── FunctionsTest.php │ ├── CollectorsTest.php │ └── StreamTest.php ├── .php_cs ├── phpunit.xml ├── grumphp.yml ├── src ├── exception │ └── InvalidStreamException.php ├── operations │ ├── AbstractCallbackOperation.php │ ├── FilterOperation.php │ ├── MappingOperation.php │ ├── LimitOperation.php │ ├── SkipOperation.php │ ├── FlatMapOperation.php │ ├── SortedOperation.php │ └── DistinctOperation.php ├── collectors │ ├── AveragingCollector.php │ └── ReducingCollector.php ├── Collector.php ├── Functions.php ├── Collectors.php └── Stream.php ├── .travis.yml ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .php_cs.cache 3 | composer.lock 4 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | exclude('vendor') 5 | ->in(__DIR__); 6 | 7 | return \PhpCsFixer\Config::create() 8 | ->setFinder($finder) 9 | ->setCacheFile(__DIR__ . '/.php_cs.cache') 10 | ->setRules([ 11 | '@PSR2' => true, 12 | ]); -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/unit 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /grumphp.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | git_dir: . 3 | bin_dir: vendor/bin 4 | tasks: 5 | composer: 6 | strict: true 7 | phpcsfixer2: 8 | config: .php_cs 9 | phpunit: ~ 10 | git_blacklist: 11 | keywords: 12 | - "die(" 13 | - "var_dump(" 14 | - "exit(" 15 | - "echo" 16 | triggered_by: [php] 17 | -------------------------------------------------------------------------------- /src/exception/InvalidStreamException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class InvalidStreamException extends \InvalidArgumentException 11 | { 12 | public function __construct( 13 | $message = "Invalid stream", 14 | $code = 0, 15 | \Exception $previous = null 16 | ) { 17 | parent::__construct($message, $code, $previous); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/operations/AbstractCallbackOperation.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class AbstractCallbackOperation extends Stream 13 | { 14 | /** 15 | * @var callable 16 | */ 17 | protected $callback; 18 | 19 | public function __construct($source, callable $callback) 20 | { 21 | parent::__construct($source); 22 | $this->callback = $callback; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/operations/FilterOperation.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class FilterOperation extends AbstractCallbackOperation 14 | { 15 | public function getIterator() 16 | { 17 | $callback = $this->callback; 18 | 19 | foreach ($this->source as $key => $value) { 20 | if ($callback($value)) { 21 | yield $key => $value; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/unit/operations/FlatMapOperationTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class FlatMapOperationTest extends TestCase 14 | { 15 | public function testFlatMap() 16 | { 17 | $instance = new FlatMapOperation([1, 2, 3], function () { 18 | return [4, 5, 6]; 19 | }); 20 | 21 | $result = $instance->toArray(); 22 | 23 | $this->assertEquals([4, 5, 6, 4, 5, 6, 4, 5, 6], $result); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/unit/operations/MappingOperationTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class MappingOperationTest extends TestCase 14 | { 15 | public function testMapping() 16 | { 17 | $mapping = new MappingOperation([1, 2, 3, 4, 5], function ($value) { 18 | return $value * 2; 19 | }); 20 | 21 | $result = iterator_to_array($mapping); 22 | 23 | $this->assertEquals([2, 4, 6, 8, 10], $result); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | cache: 4 | directories: 5 | - ~/.cache/composer 6 | 7 | env: 8 | global: 9 | # Needed because of PHP 7.3 10 | - PHP_CS_FIXER_IGNORE_ENV=1 11 | 12 | matrix: 13 | fast_finish: true 14 | include: 15 | - php: 7.0 16 | - php: 7.1 17 | - php: 7.2 18 | - php: 7.3 19 | env: COMPOSERFLAGS="" 20 | - php: 7.3 21 | env: COMPOSERFLAGS="--ignore-platform-reqs" 22 | - php: nightly 23 | 24 | allow_failures: 25 | - php: nightly 26 | - php: 7.3 27 | env: COMPOSERFLAGS="" 28 | 29 | install: 30 | - composer install $COMPOSERFLAGS 31 | 32 | script: 33 | - vendor/bin/grumphp run 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bertptrs/phpstreams", 3 | "description": "A streams library for PHP based on the Java 8 Streams API.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Bert Peters", 8 | "email": "bert@bertptrs.nl" 9 | } 10 | ], 11 | "require": { 12 | "php": "^7.0" 13 | }, 14 | "require-dev": { 15 | "phpunit/phpunit": "^6.0", 16 | "phpro/grumphp": "^0.14.3", 17 | "friendsofphp/php-cs-fixer": "^2.13" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "phpstreams\\": "src" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "phpstreams\\tests\\": "tests" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/collectors/AveragingCollector.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class AveragingCollector implements Collector 18 | { 19 | private $runningSum = 0.0; 20 | 21 | private $count = 0; 22 | 23 | public function add($key, $value) 24 | { 25 | $this->runningSum += $value; 26 | $this->count++; 27 | } 28 | 29 | public function get() 30 | { 31 | return $this->runningSum / $this->count; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/unit/operations/FilterOperationTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class FilterOperationTest extends TestCase 14 | { 15 | public function testFiltering() 16 | { 17 | $filter = new FilterOperation( 18 | [1, 2, 3, 4, 5], 19 | function ($value) { 20 | return $value % 2 == 0; 21 | } 22 | ); 23 | 24 | $result = iterator_to_array($filter); 25 | 26 | $this->assertEquals([ 27 | 1 => 2, 28 | 3 => 4 29 | ], $result); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/unit/operations/LimitOperationTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class LimitOperationTest extends TestCase 15 | { 16 | public function testLimit() 17 | { 18 | $instance = new LimitOperation([1, 2, 3, 4], 2); 19 | 20 | $result = iterator_to_array($instance); 21 | 22 | $this->assertEquals([1, 2], $result); 23 | } 24 | 25 | /** 26 | * @expectedException InvalidArgumentException 27 | */ 28 | public function testInvalidLimit() 29 | { 30 | new LimitOperation([], -1); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Collector.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface Collector 16 | { 17 | /** 18 | * Add a new value to the collector. 19 | * 20 | * @param mixed $key The current key. 21 | * @param mixed $value The current value. 22 | */ 23 | public function add($key, $value); 24 | 25 | /** 26 | * Get the final result from the collector. 27 | * 28 | * @return mixed Whatever the result of this collector is. 29 | */ 30 | public function get(); 31 | } 32 | -------------------------------------------------------------------------------- /src/operations/MappingOperation.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class MappingOperation extends AbstractCallbackOperation 13 | { 14 | public function getIterator() 15 | { 16 | $callback = $this->callback; 17 | 18 | foreach ($this->source as $key => $value) { 19 | yield $key => $callback($value); 20 | } 21 | } 22 | 23 | /** 24 | * Override the parent sorting method. 25 | * 26 | * Mapping does not neccesarily preserve sorting order, so this method 27 | * always returns false. 28 | * 29 | * @return boolean 30 | */ 31 | public function isSorted() 32 | { 33 | return false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/unit/operations/SkipOperationTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class SkipOperationTest extends TestCase 15 | { 16 | public function testSkip() 17 | { 18 | $instance = new SkipOperation([1, 2, 3, 4], 2); 19 | 20 | $result = iterator_to_array($instance); 21 | 22 | $expected = [ 23 | 2 => 3, 24 | 3 => 4, 25 | ]; 26 | 27 | $this->assertEquals($expected, $result); 28 | } 29 | 30 | /** 31 | * @expectedException InvalidArgumentException 32 | */ 33 | public function testInvalidSkip() 34 | { 35 | new SkipOperation([], -3); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Functions.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | abstract class Functions 11 | { 12 | /** 13 | * Get an identity function. 14 | * 15 | * @return callable A function that returns its first argument. 16 | */ 17 | public static function identity() 18 | { 19 | return function ($value) { 20 | return $value; 21 | }; 22 | } 23 | 24 | /** 25 | * Combine two functions into one. 26 | * 27 | * This method creates a function that effectively is b(a(value)). 28 | * 29 | * @param callable $a 30 | * @param callable $b 31 | * @return callable 32 | */ 33 | public static function combine(callable $a, callable $b) 34 | { 35 | return function ($value) use ($a, $b) { 36 | return $b($a($value)); 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/collectors/ReducingCollector.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ReducingCollector implements Collector 13 | { 14 | /** 15 | * @var mixed The current value in the reductor. 16 | */ 17 | private $current; 18 | 19 | /** 20 | * The reduction being applied. 21 | * 22 | * @var callable 23 | */ 24 | private $reductor; 25 | 26 | public function __construct($identity, callable $reductor) 27 | { 28 | $this->current = $identity; 29 | 30 | $this->reductor = $reductor; 31 | } 32 | 33 | public function add($key, $value) 34 | { 35 | $reductor = $this->reductor; 36 | 37 | $this->current = $reductor($this->current, $value); 38 | } 39 | 40 | public function get() 41 | { 42 | return $this->current; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Bert Peters 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/operations/LimitOperation.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class LimitOperation extends Stream 14 | { 15 | private $limit; 16 | 17 | /** 18 | * Construct a limited stream. 19 | * 20 | * @param type $source 21 | * @param type $limit The number of items to show, at most. 22 | * @throws InvalidArgumentException 23 | */ 24 | public function __construct($source, $limit) 25 | { 26 | parent::__construct($source); 27 | 28 | if ($limit < 0) { 29 | throw new InvalidArgumentException("Limit should be at least 0."); 30 | } 31 | 32 | $this->limit = $limit; 33 | } 34 | 35 | public function getIterator() 36 | { 37 | $limit = $this->limit; 38 | 39 | foreach ($this->source as $key => $value) { 40 | if (--$limit < 0) { 41 | return; 42 | } 43 | 44 | yield $key => $value; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/unit/operations/DistinctOperationTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class DistinctOperationTest extends TestCase 15 | { 16 | public function testDistinct() 17 | { 18 | $data = [1, 4, 2, 3, '1', 3]; 19 | 20 | $instance = new DistinctOperation($data, false); 21 | 22 | $result = iterator_to_array($instance); 23 | 24 | $this->assertEquals([ 25 | 0 => 1, 26 | 1 => 4, 27 | 2 => 2, 28 | 3 => 3, 29 | ], $result); 30 | } 31 | 32 | public function testStrictDistinct() 33 | { 34 | $data = [1, 4, 2, 3, '1', 3]; 35 | 36 | $instance = new DistinctOperation($data, true); 37 | 38 | $result = iterator_to_array($instance); 39 | 40 | $this->assertEquals([ 41 | 0 => 1, 42 | 1 => 4, 43 | 2 => 2, 44 | 3 => 3, 45 | 4 => '1', 46 | ], $result); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/operations/SkipOperation.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class SkipOperation extends Stream 15 | { 16 | /** 17 | * @var int 18 | */ 19 | private $toSkip; 20 | 21 | /** 22 | * Construct a new skipping stream. 23 | * 24 | * @param Traversable $source 25 | * @param int $toSkip The number of items to skip. 26 | * @throws InvalidArgumentException if the number to skip is less than 0. 27 | */ 28 | public function __construct($source, $toSkip) 29 | { 30 | parent::__construct($source); 31 | 32 | if ($toSkip < 0) { 33 | throw new InvalidArgumentException("To skip should be >= 0."); 34 | } 35 | 36 | $this->toSkip = $toSkip; 37 | } 38 | 39 | public function getIterator() 40 | { 41 | $toSkip = $this->toSkip; 42 | 43 | foreach ($this->source as $key => $value) { 44 | if ($toSkip > 0) { 45 | $toSkip--; 46 | continue; 47 | } 48 | 49 | yield $key => $value; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/unit/FunctionsTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class FunctionsTest extends TestCase 14 | { 15 | 16 | /** 17 | * Test the Functions::identity method. 18 | */ 19 | public function testIdentity() 20 | { 21 | $data = [ 22 | 'foo', 23 | 'bar', 24 | 42, 25 | ]; 26 | 27 | $identityFunc = Functions::identity(); 28 | 29 | $this->assertTrue(is_callable($identityFunc), "Identity function should be callable."); 30 | 31 | foreach ($data as $item) { 32 | $this->assertEquals($item, $identityFunc($item)); 33 | } 34 | } 35 | 36 | /** 37 | * Test the Functions::combine method. 38 | */ 39 | public function testCombine() 40 | { 41 | $a = function ($value) { 42 | return $value + 2; 43 | }; 44 | 45 | $b = function ($value) { 46 | return $value * 3; 47 | }; 48 | 49 | $result = Functions::combine($a, $b); 50 | 51 | $this->assertTrue(is_callable($result), 'Combined function should be callable.'); 52 | 53 | $this->assertEquals($result(3), 15); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/unit/CollectorsTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class CollectorsTest extends TestCase 14 | { 15 | public function testAveraging() 16 | { 17 | $instance = Collectors::averaging(); 18 | 19 | $instance->add("a", 6); 20 | $instance->add("b", 9); 21 | $instance->add("21", 4); 22 | $instance->add("foo", 21); 23 | 24 | $this->assertEquals(10, $instance->get()); 25 | } 26 | 27 | public function testJoining() 28 | { 29 | $instance = Collectors::joining(); 30 | 31 | $instance->add("a", "b"); 32 | $instance->add("foo", "bar"); 33 | $instance->add(1, 2); 34 | 35 | $this->assertEquals("bbar2", $instance->get()); 36 | } 37 | 38 | public function testJoiningWithDelimiter() 39 | { 40 | $instance = Collectors::joining(","); 41 | 42 | $instance->add("a", "b"); 43 | $instance->add("foo", "bar"); 44 | $instance->add(1, 2); 45 | 46 | $this->assertEquals("b,bar,2", $instance->get()); 47 | } 48 | 49 | public function testReducing() 50 | { 51 | $instance = Collectors::reducing(1, function ($a, $b) { 52 | return $a * $b; 53 | }); 54 | 55 | $instance->add("foo", 2); 56 | $instance->add("bar", 3); 57 | $instance->add("baz", 4); 58 | 59 | $this->assertEquals(24, $instance->get()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/operations/FlatMapOperation.php: -------------------------------------------------------------------------------- 1 | value relationships, as this would not 11 | * make sense with nested arrays. Instead, it produces sequential keys. 12 | * 13 | * @author Bert Peters 14 | */ 15 | class FlatMapOperation extends Stream 16 | { 17 | /** 18 | * @var callable Callback used to unpack items in the stream. 19 | */ 20 | private $unpacker; 21 | 22 | /** 23 | * Construct a new flatmap stream. 24 | * 25 | * @param iterable $source The source to read from. 26 | * @param callable $unpacker Some callback that convert source stream items 27 | * to something iterable. 28 | */ 29 | public function __construct($source, callable $unpacker) 30 | { 31 | parent::__construct($source); 32 | 33 | $this->unpacker = $unpacker; 34 | } 35 | 36 | public function getIterator() 37 | { 38 | $unpacker = $this->unpacker; 39 | 40 | foreach ($this->source as $value) { 41 | foreach ($unpacker($value) as $unpackedValue) { 42 | yield $unpackedValue; 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Override the parent sorting method. 49 | * 50 | * Flatmapping does not neccesarily preserve sorting order, so this method 51 | * always returns false. 52 | * 53 | * @return boolean 54 | */ 55 | public function isSorted() 56 | { 57 | return false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/operations/SortedOperation.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class SortedOperation extends Stream 22 | { 23 | /** 24 | * @var callable Sort comparator. 25 | */ 26 | private $sort; 27 | 28 | public function __construct($source, callable $sort = null) 29 | { 30 | parent::__construct($source); 31 | 32 | $this->sort = $sort; 33 | } 34 | 35 | public function getIterator() 36 | { 37 | // Convert the source to an array, for sorting. 38 | if ($this->source instanceof Traversable) { 39 | $data = iterator_to_array($this->source); 40 | } elseif (is_array($this->source)) { 41 | $data = $this->source; 42 | } else { 43 | throw new InvalidStreamException("Cannot handle stream of type ".gettype($this->source)); 44 | } 45 | 46 | if ($this->sort != null) { 47 | uasort($data, $this->sort); 48 | } else { 49 | asort($data); 50 | } 51 | 52 | return new ArrayIterator($data); 53 | } 54 | 55 | public function isSorted() 56 | { 57 | return true; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/unit/operations/SortedOperationTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class SortedOperationTest extends TestCase 15 | { 16 | public function testSorted() 17 | { 18 | $instance = new SortedOperation([6, 2, 9, 4, 7, 12, 3, 1]); 19 | 20 | $result = iterator_to_array($instance); 21 | $expected = [ 22 | 7 => 1, 23 | 1 => 2, 24 | 6 => 3, 25 | 3 => 4, 26 | 0 => 6, 27 | 4 => 7, 28 | 2 => 9, 29 | 5 => 12, 30 | ]; 31 | 32 | $this->assertEquals($expected, $result); 33 | } 34 | 35 | public function testSortedWithIterator() 36 | { 37 | $instance = new SortedOperation(new ArrayIterator([6, 2, 9, 4, 7, 12, 3, 1])); 38 | 39 | $result = iterator_to_array($instance); 40 | $expected = [ 41 | 7 => 1, 42 | 1 => 2, 43 | 6 => 3, 44 | 3 => 4, 45 | 0 => 6, 46 | 4 => 7, 47 | 2 => 9, 48 | 5 => 12, 49 | ]; 50 | 51 | $this->assertEquals($expected, $result); 52 | } 53 | 54 | public function testSortedWithCallback() 55 | { 56 | // Test by sorting modulo 7. 57 | $instance = new SortedOperation([6, 21, 17, 44, 40], function ($a, $b) { 58 | if ($a % 7 < $b % 7) { 59 | return -1; 60 | } 61 | 62 | return $a % 7 > $b % 7; 63 | }); 64 | 65 | $result = iterator_to_array($instance); 66 | 67 | $expected = [ 68 | 1 => 21, 69 | 3 => 44, 70 | 2 => 17, 71 | 4 => 40, 72 | 0 => 6, 73 | ]; 74 | 75 | $this->assertEquals($expected, $result); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/operations/DistinctOperation.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class DistinctOperation extends Stream 15 | { 16 | private $strict; 17 | 18 | /** 19 | * Construct a unique stream. 20 | * 21 | * This stream will yield each distinct value only once. 22 | * 23 | * @param \Traversable $source 24 | * @param boolean $strict Whether to use strict comparisons when determining uniqueness. 25 | */ 26 | public function __construct($source, $strict) 27 | { 28 | parent::__construct($source); 29 | 30 | $this->strict = $strict; 31 | } 32 | 33 | public function getIterator() 34 | { 35 | return $this->isSorted() 36 | ? $this->sortedIterator() 37 | : $this->unsortedIterator(); 38 | } 39 | 40 | /** 41 | * Iterator for unsorted streams. 42 | * 43 | * This iterator compares the value to all previously found values to 44 | * decide on uniqueness. 45 | */ 46 | private function unsortedIterator() 47 | { 48 | $data = []; 49 | 50 | foreach ($this->source as $key => $value) { 51 | if (!in_array($value, $data, $this->strict)) { 52 | $data[] = $value; 53 | yield $key => $value; 54 | } 55 | } 56 | } 57 | 58 | /** 59 | * Iterator for sorted streams. 60 | * 61 | * This method compares the current value to the previous value to decide on 62 | * uniqueness. 63 | */ 64 | private function sortedIterator() 65 | { 66 | $prev = null; 67 | $first = true; 68 | 69 | foreach ($this->source as $key => $value) { 70 | if ($first || ($this->strict && $value !== $prev) || (!$this->strict && $value != $prev)) { 71 | yield $key => $value; 72 | $first = false; 73 | $prev = $value; 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Collectors.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class Collectors 18 | { 19 | /** 20 | * Hidden constructor, static class. 21 | */ 22 | private function __construct() 23 | { 24 | } 25 | 26 | /** 27 | * Collector that returns the average of the given elements. 28 | * 29 | * The return type of this collector is float, regardless of the type of 30 | * the stream elements. 31 | * 32 | * @return Collector 33 | */ 34 | public static function averaging() 35 | { 36 | return new AveragingCollector(); 37 | } 38 | 39 | /** 40 | * Get a collector that applies the given reduction to the stream. 41 | * 42 | * @param mixed $identity 43 | * @param callable $reduction 44 | * @return Collector 45 | */ 46 | public static function reducing($identity, callable $reduction) 47 | { 48 | return new ReducingCollector($identity, $reduction); 49 | } 50 | 51 | /** 52 | * Get a collector that concatenates all the elements in the stream. 53 | * 54 | * @param string $delimiter [optional] A delimiter to insert between 55 | * elements. Defaults to the empty string. 56 | * @return Collector 57 | */ 58 | public static function joining($delimiter = "") 59 | { 60 | $first = true; 61 | 62 | return Collectors::reducing( 63 | "", 64 | function ($current, $element) use (&$first, $delimiter) { 65 | if (!$first) { 66 | $current .= $delimiter; 67 | } else { 68 | $first = false; 69 | } 70 | 71 | return $current . $element; 72 | } 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPStreams 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/bertptrs/phpstreams/v/stable)](https://packagist.org/packages/bertptrs/phpstreams) [![Total Downloads](https://poser.pugx.org/bertptrs/phpstreams/downloads)](https://packagist.org/packages/bertptrs/phpstreams) [![License](https://poser.pugx.org/bertptrs/phpstreams/license)](https://packagist.org/packages/bertptrs/phpstreams) [![Build Status](https://travis-ci.org/bertptrs/phpstreams.svg?branch=master)](https://travis-ci.org/bertptrs/phpstreams) 4 | 5 | A partial implementation of the Java 8 Streams API in PHP. PHPStreams 6 | can use your generators, your arrays and really anything that is 7 | [Iterable](https://wiki.php.net/rfc/iterable) and convert modify it like 8 | you're used to using Java Streams! 9 | 10 | Using streams and generators, you can easily sort through large amounts 11 | of data without having to have it all in memory or in scope. Streams 12 | also make it easier to structure your code, by (more or less) enforcing 13 | single resposibility. 14 | 15 | The library is compatible with PHP 5.5.9 and up. 16 | 17 | ## Installation 18 | 19 | PHPStreams can be installed using Composer. Just run `composer require 20 | bertptrs/phpstreams` in your project root! 21 | 22 | ## Usage 23 | 24 | Using streams is easy. Say, we want the first 7 odd numbers in the 25 | Fibonacci sequence. To do this using Streams, we do the following: 26 | 27 | ```php 28 | // Define a generator for Fibonacci numbers 29 | function fibonacci() 30 | { 31 | yield 0; 32 | yield 1; 33 | 34 | $prev = 0; 35 | $cur = 1; 36 | 37 | while (true) { 38 | yield ($new = $cur + $prev); 39 | $prev = $cur; 40 | $cur = $new; 41 | } 42 | }; 43 | 44 | // Define a predicate that checks for odd numbers 45 | $isOdd = function($num) { 46 | return $num % 2 == 1; 47 | }; 48 | 49 | // Create our stream. 50 | $stream = new phpstreams\Stream(fibonacci()); 51 | 52 | // Finally, use these to create our result. 53 | $oddFibo = $stream->filter($isOdd) // Keep only the odd numbers 54 | ->limit(8) // Limit our results 55 | ->toArray(false); // Convert to array, discarding keys 56 | ``` 57 | 58 | ## Documentation 59 | 60 | Documentation is mostly done using PHPDoc. I do intend to write actual 61 | documtation if there is any interest. 62 | 63 | ## Contributing 64 | 65 | I welcome contributions and pull requests. Please note that I do follow 66 | PSR-2 (and PSR-4 for autoloading). Also, please submit unit tests with 67 | your work. 68 | 69 | GrumPHP enforces at least part of the coding standard, but do make an 70 | effort to structure your contributions nicely. 71 | -------------------------------------------------------------------------------- /tests/unit/StreamTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class StreamTest extends TestCase 15 | { 16 | 17 | /** 18 | * Test whether the source checker actually works. 19 | */ 20 | public function testIsValidSource() 21 | { 22 | $this->assertTrue( 23 | Stream::isValidSource([]), 24 | 'Arrays should be valid sources.' 25 | ); 26 | 27 | $this->assertTrue( 28 | Stream::isValidSource(new DirectoryIterator(__DIR__)), 29 | 'Iterators should be valid sources.' 30 | ); 31 | 32 | $this->assertFalse( 33 | Stream::isValidSource("Not a stream"), 34 | "Strings should be invalid sources." 35 | ); 36 | } 37 | 38 | public function testConstructor() 39 | { 40 | $value = new Stream([]); 41 | 42 | $this->assertInstanceOf("Traversable", $value); 43 | } 44 | 45 | /** 46 | * @test 47 | * @expectedException phpstreams\exception\InvalidStreamException 48 | */ 49 | public function testConstructorException() 50 | { 51 | $value = new Stream("Geen source."); 52 | } 53 | 54 | public function testAll() 55 | { 56 | $stream = new Stream([1, 4, 9, 16, 25]); 57 | 58 | // All these integers are truthy. 59 | $this->assertTrue($stream->all()); 60 | 61 | // Not all of them are even. 62 | $this->assertFalse($stream->all(function ($value) { 63 | return $value % 2 == 0; 64 | })); 65 | 66 | // All of these are squares. 67 | $this->assertTrue($stream->all(function ($value) { 68 | $root = round(sqrt($value)); 69 | 70 | return $root * $root == $value; 71 | })); 72 | } 73 | 74 | public function testAny() 75 | { 76 | $this->assertTrue((new Stream([false, false, true, false]))->any()); 77 | 78 | $this->assertFalse((new Stream([false, false, false, false]))->any()); 79 | 80 | $stream = new Stream([1, 2, 3, 4, 42]); 81 | 82 | $this->assertTrue($stream->any(function ($value) { 83 | return $value == 42; 84 | })); 85 | } 86 | 87 | public function testCount() 88 | { 89 | $stream = new Stream([1, 2, 3, 4, 5, 6]); 90 | 91 | $this->assertEquals(6, $stream->count()); 92 | $this->assertEquals(6, count($stream)); 93 | 94 | $filteredStream = $stream->filter(function ($value) { 95 | return $value % 2 == 1; 96 | }); 97 | 98 | $this->assertEquals(3, $filteredStream->count()); 99 | } 100 | 101 | public function testToArray() 102 | { 103 | $stream = new Stream([1, 2, 3, 4, 5, 6]); 104 | 105 | $result = $stream->skip(4)->toArray(); 106 | 107 | $this->assertEquals([ 108 | 4 => 5, 109 | 5 => 6 110 | ], $result); 111 | 112 | $this->assertEquals([5, 6], $stream->skip(4)->toArray(false)); 113 | } 114 | 115 | public function testReduce() 116 | { 117 | $instance = new Stream([1, 2, 3, 4]); 118 | 119 | $result = $instance->reduce(0, function ($a, $b) { 120 | return $a + $b; 121 | }); 122 | 123 | $this->assertEquals(10, $result); 124 | } 125 | 126 | /** 127 | * Test the collect method. 128 | */ 129 | public function testCollect() 130 | { 131 | $instance = new Stream([1, 2, 3, 4]); 132 | 133 | $collector = $this->getMockBuilder('phpstreams\Collector') 134 | ->getMock(); 135 | 136 | $collector->expects($this->exactly(4)) 137 | ->method('add'); 138 | 139 | $collector->expects($this->once()) 140 | ->method('get') 141 | ->willReturn(42); 142 | 143 | $this->assertEquals(42, $instance->collect($collector)); 144 | } 145 | 146 | public function testIsSortedWithSortedSource() 147 | { 148 | $sortedSource = $this->getMockBuilder('phpstreams\Stream') 149 | ->disableOriginalConstructor() 150 | ->getMock(); 151 | 152 | $sortedSource->expects($this->once()) 153 | ->method('isSorted') 154 | ->willReturn(true); 155 | 156 | $instance = new Stream($sortedSource); 157 | 158 | $this->assertTrue($instance->isSorted()); 159 | } 160 | 161 | public function testIsSortedWithUnsortedSource() 162 | { 163 | $unsortedSource = $this->getMockBuilder('phpstreams\Stream') 164 | ->disableOriginalConstructor() 165 | ->getMock(); 166 | 167 | $unsortedSource->expects($this->once()) 168 | ->method('isSorted') 169 | ->willReturn(false); 170 | 171 | $instance = new Stream($unsortedSource); 172 | 173 | $this->assertFalse($instance->isSorted()); 174 | } 175 | 176 | public function testIsSortedWithArray() 177 | { 178 | $instance = new Stream([]); 179 | 180 | $this->assertFalse($instance->isSorted()); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Stream.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class Stream implements IteratorAggregate, Countable 23 | { 24 | /** 25 | * @var Traversable Backing source for this Stream. 26 | */ 27 | protected $source; 28 | 29 | /** 30 | * Construct a new stream from a traversable source. 31 | * 32 | * @param Traversable|array $source The source to create the stream from. 33 | * @throws InvalidStreamException if the given source is not usable as a stream. 34 | */ 35 | public function __construct($source) 36 | { 37 | if (!static::isValidSource($source)) { 38 | throw new InvalidStreamException(); 39 | } 40 | 41 | $this->source = $source; 42 | } 43 | 44 | /** 45 | * Check whether a source is valid. 46 | * 47 | * Valid sources are either an array or a Traversable. 48 | * 49 | * @param mixed $source 50 | * @return boolean True if it is. 51 | */ 52 | public static function isValidSource($source) 53 | { 54 | return is_array($source) || ($source instanceof Traversable); 55 | } 56 | 57 | public function getIterator() 58 | { 59 | foreach ($this->source as $key => $value) { 60 | yield $key => $value; 61 | } 62 | } 63 | 64 | /** 65 | * Apply a filter to the current stream. 66 | * 67 | * Note that filter, like most stream operations, preserves the key => value relationship throughout its execution. That means that there will be missing keys, if you started with a regular array. 68 | * 69 | * @param callable $filter 70 | * @return Stream a new stream yielding only the elements for which the callback returns true. 71 | */ 72 | public function filter(callable $filter) 73 | { 74 | return new FilterOperation($this, $filter); 75 | } 76 | 77 | /** 78 | * Map the values in the stream to another stream. 79 | * 80 | * @param callable $mapping 81 | * @return Stream a new stream with the variables mapped. 82 | */ 83 | public function map(callable $mapping) 84 | { 85 | return new MappingOperation($this, $mapping); 86 | } 87 | 88 | /** 89 | * Enforce distinct values. 90 | * 91 | * This stream will yield every distinct value only once. Note that 92 | * internally, it uses in_array for lookups. This is problematic for large 93 | * numbers of distinct elements, as the complexity of this stream filter 94 | * becomes O(n * m) with n the number of elements and m the number of distinct 95 | * values. 96 | * 97 | * This mapping preserves key => value relationships, and will yield the 98 | * values with the first keys encountered. 99 | * 100 | * @param type $strict whether to use strict comparisons for uniqueness. 101 | * @return Stream 102 | */ 103 | public function distinct($strict = false) 104 | { 105 | return new DistinctOperation($this, $strict); 106 | } 107 | 108 | /** 109 | * Get a sorted view of the stream. 110 | * 111 | * This method yields its values sorted. Sort is done using the default asort() 112 | * function, or uasort if a sorting function was given. 113 | * 114 | * Note that by the nature of sorting things, the entire previous source will be 115 | * read into memory in order to sort it. This may be a performance issue. 116 | * 117 | * @param callable $sort [optional] a callback to use for sorting. 118 | * @return Stream 119 | */ 120 | public function sorted(callable $sort = null) 121 | { 122 | return new SortedOperation($this, $sort); 123 | } 124 | 125 | /** 126 | * Skip the first few elements. 127 | * 128 | * This method discards the first few elements from the stream, maintaining 129 | * key => value relations. 130 | * 131 | * @param int $toSkip 132 | * @return Stream 133 | */ 134 | public function skip($toSkip) 135 | { 136 | return new SkipOperation($this, $toSkip); 137 | } 138 | 139 | /** 140 | * Limit the stream to some number of elements. 141 | * 142 | * All elements after the limit are not considered or generated, so this can 143 | * be used with infinite streams. 144 | * 145 | * @param int $limit The maximum number of elements to return. 146 | * @return Stream 147 | */ 148 | public function limit($limit) 149 | { 150 | return new LimitOperation($this, $limit); 151 | } 152 | 153 | /** 154 | * Return true if any element matches the predicate. 155 | * 156 | * This method short-circuits, so if any item matches the predicate, no 157 | * further items are evaluated. 158 | * 159 | * @param callable $predicate 160 | * @return boolean 161 | */ 162 | public function any(callable $predicate = null) 163 | { 164 | if ($predicate === null) { 165 | $predicate = Functions::identity(); 166 | } 167 | 168 | foreach ($this as $value) { 169 | if ($predicate($value)) { 170 | return true; 171 | } 172 | } 173 | 174 | return false; 175 | } 176 | 177 | /** 178 | * Return true if all element matches the predicate. 179 | * 180 | * This method short-circuits, so if any item does not match the predicate, 181 | * no further elements are considered. 182 | * 183 | * @param callable $predicate 184 | * @return boolean 185 | */ 186 | public function all(callable $predicate = null) 187 | { 188 | if ($predicate === null) { 189 | $predicate = Functions::identity(); 190 | } 191 | 192 | foreach ($this as $value) { 193 | if (!$predicate($value)) { 194 | return false; 195 | } 196 | } 197 | 198 | return true; 199 | } 200 | 201 | /** 202 | * Convert this stream to an array. 203 | * 204 | * @param boolean $withKeys [optional] if false, return a simple array containing all values in order. If true (the default) preserve keys. 205 | * @return array An (associative) array of the contents of the stream. 206 | */ 207 | public function toArray($withKeys = true) 208 | { 209 | return iterator_to_array($this, $withKeys); 210 | } 211 | 212 | /** 213 | * Count the number of elements in this stream. 214 | * 215 | * This method may consume the stream, if it is not repeatable. Use it only 216 | * when appropriate. 217 | * 218 | * As this is simply an implementation of the Countable interface, the count 219 | * function may be used instead. However, this still may consume the stream. 220 | * 221 | * @return int The number of elements in this Stream. 222 | */ 223 | public function count() 224 | { 225 | return iterator_count($this); 226 | } 227 | 228 | /** 229 | * Reduce the stream using the given binary operation. 230 | * 231 | * The given is applied to every item in the stream in no particular order. 232 | * The result is then returned. 233 | * 234 | * In order for the callable to be a proper reductor, it should be: 235 | * 240 | * If any of these properties do not hold, the output of this function is 241 | * not defined. 242 | * 243 | * @param mixed $identity The identity element. 244 | * @param callable $binaryOp A reduction function, respecting the properties 245 | * above. 246 | * @return mixed 247 | */ 248 | public function reduce($identity, callable $binaryOp) 249 | { 250 | $cur = $identity; 251 | 252 | foreach ($this as $value) { 253 | $cur = $binaryOp($cur, $value); 254 | } 255 | 256 | return $cur; 257 | } 258 | 259 | public function collect(Collector $collector) 260 | { 261 | foreach ($this as $key => $value) { 262 | $collector->add($key, $value); 263 | } 264 | 265 | return $collector->get(); 266 | } 267 | 268 | /** 269 | * Flatten the underlying stream. 270 | * 271 | * This method takes each element and unpacks it into a sequence of elements. 272 | * All individual sequences are concatenated in the resulting stream. 273 | * 274 | * @param callable $unpacker [optional] An unpacker function that can unpack 275 | * elements into something iterable. Default is to use the identity function. 276 | * @return Stream 277 | */ 278 | public function flatMap(callable $unpacker = null) 279 | { 280 | if ($unpacker == null) { 281 | $unpacker = Functions::identity(); 282 | } 283 | 284 | return new FlatMapOperation($this, $unpacker); 285 | } 286 | 287 | /** 288 | * Check whether this stream is definitely sorted. 289 | * 290 | * This is used to optimize some stream operations, such as distinct(), 291 | * which only needs constant memory when operating on a sorted list. 292 | * 293 | * Any stream operations that potentially change the sorting order should 294 | * override this method to properly reflect the actual sorting order. 295 | * 296 | * @return boolean 297 | */ 298 | public function isSorted() 299 | { 300 | if ($this->source instanceof Stream) { 301 | return $this->source->isSorted(); 302 | } else { 303 | return false; 304 | } 305 | } 306 | } 307 | --------------------------------------------------------------------------------