├── .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 | [](https://packagist.org/packages/bertptrs/phpstreams) [](https://packagist.org/packages/bertptrs/phpstreams) [](https://packagist.org/packages/bertptrs/phpstreams) [](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 | *
236 | * - Commutative, so op($a, $b) is equal to op($b, $a), and
237 | *
- Should preserve respect the given identity, i.e. op($a, $identity) =
238 | * $identity.
239 | *
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 |
--------------------------------------------------------------------------------