Skip to content

Commit b1a681b

Browse files
committed
New iterable factory
1 parent 9d25ea2 commit b1a681b

11 files changed

+542
-5
lines changed

.travis.yml

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
language: php
22

33
php:
4-
- 5.3
54
- 5.4
65
- 5.5
76
- 5.6
87
- 7.0
98
- 7.1
9+
- 7.2
10+
11+
matrix:
12+
include:
13+
- php: 5.3
14+
dist: precise
1015

1116
before_script:
1217
- travis_retry composer self-update
1318
- travis_retry composer install --no-interaction --prefer-source --dev
1419
- travis_retry phpenv rehash
1520

1621
script:
17-
- ./vendor/bin/phpcs --standard=psr2 src/
18-
- mkdir -p build/logs
22+
- if [[ ${TRAVIS_PHP_VERSION:0:3} == "5.3" ]]; then composer require --dev symfony/polyfill-php54; fi
1923
- ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml
24+
- ./vendor/bin/phpcs --standard=psr2 -n src/
25+
- mkdir -p build/logs
2026

2127
after_script:
2228
- php vendor/bin/coveralls -v

README.md

+99-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ To check wether or not a PHP variable can be looped over in a `foreach` statemen
1616

1717
**But this function only works on PHP7.1+**.
1818

19-
This library ships a portage of this function for previous PHP versions.
19+
This library ships a polyfill of this function for previous PHP versions.
2020

2121
Usage:
2222
```php
@@ -55,6 +55,104 @@ var_dump(iterable_to_traversable(array('foo', 'bar'))); // ArrayIterator(array('
5555
var_dump(iterable_to_traversable(new ArrayIterator(array('foo', 'bar')))); // ArrayIterator(array('foo', 'bar'))
5656
```
5757

58+
59+
iterable_map()
60+
--------------
61+
62+
Works like an `array_map` with an `array` or a `Traversable`.
63+
64+
```php
65+
$generator = function () {
66+
yield 'foo';
67+
yield 'bar';
68+
};
69+
70+
foreach (iterable_map($generator(), 'strtoupper') as $item) {
71+
var_dump($item); // FOO, BAR
72+
}
73+
```
74+
75+
iterable_filter()
76+
--------------
77+
78+
Works like an `array_filter` with an `array` or a `Traversable`.
79+
80+
```php
81+
$generator = function () {
82+
yield 0;
83+
yield 1;
84+
};
85+
86+
foreach (iterable_filter($generator()) as $item) {
87+
var_dump($item); // 1
88+
}
89+
```
90+
91+
Of course you can define your own filter:
92+
```php
93+
$generator = function () {
94+
yield 'foo';
95+
yield 'bar';
96+
};
97+
98+
$filter = function ($value) {
99+
return 'foo' !== $value;
100+
};
101+
102+
103+
foreach (iterable_filter($generator(), $filter) as $item) {
104+
var_dump($item); // bar
105+
}
106+
```
107+
108+
109+
Iterable factory
110+
================
111+
112+
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:
113+
114+
```php
115+
interface SomeInterface
116+
{
117+
/**
118+
* Return an iterable list of items
119+
*
120+
* @return iterable
121+
*/
122+
public function getItems(): iterable;
123+
}
124+
125+
class MyService implements SomeInterface
126+
{
127+
/**
128+
* @inheritdoc
129+
*/
130+
public function getItems(): iterable
131+
{
132+
return iterable($this->someOtherService->findAll()):
133+
}
134+
135+
}
136+
```
137+
138+
It even accepts a `null` value (then converting it to an `EmptyIterator`).
139+
140+
You may add a `filter` callable and a `map` callable to make your life easier:
141+
142+
```php
143+
$data = [
144+
'banana',
145+
'pineapple',
146+
'potato',
147+
];
148+
149+
$isFruit = function ($eatable) {
150+
return 'potato' !== $eatable;
151+
};
152+
153+
var_dump(iterator_to_array(iterable($data)->withFilter($isFruit)->withMap('strtoupper'))); // ['banana', 'pineapple']
154+
```
155+
58156
Installation
59157
============
60158

composer.json

+10-1
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,23 @@
44
"type": "library",
55
"license": "MIT",
66
"autoload": {
7+
"psr-4": {
8+
"BenTools\\IterableFunctions\\": "src"
9+
},
710
"files": ["src/iterable-functions.php"]
811
},
12+
"autoload-dev": {
13+
"files": [
14+
"vendor/symfony/var-dumper/Resources/functions/dump.php"
15+
]
16+
},
917
"require": {
1018
"php": ">=5.3"
1119
},
1220
"require-dev": {
1321
"phpunit/phpunit": "@stable",
1422
"squizlabs/php_codesniffer": "@stable",
15-
"satooshi/php-coveralls": "@stable"
23+
"satooshi/php-coveralls": "@stable",
24+
"symfony/var-dumper": "@stable"
1625
}
1726
}

phpunit.xml.dist

+7
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,18 @@
2020
<file>tests/TestIsIterable.php</file>
2121
<file>tests/TestIterableToArray.php</file>
2222
<file>tests/TestIterableToTraversable.php</file>
23+
<file>tests/TestIterableFilter.php</file>
24+
<file>tests/TestIterableMap.php</file>
25+
<file>tests/TestIterableObject.php</file>
2326
</testsuite>
2427
</testsuites>
2528
<filter>
2629
<whitelist processUncoveredFilesFromWhitelist="true">
2730
<directory suffix=".php">./src</directory>
31+
<exclude>
32+
<file>src/iterable-map-php53.php</file>
33+
<file>src/iterable-map-php55.php</file>
34+
</exclude>
2835
</whitelist>
2936
</filter>
3037
</phpunit>

src/IterableObject.php

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
namespace BenTools\IterableFunctions;
4+
5+
use Closure;
6+
use EmptyIterator;
7+
use InvalidArgumentException;
8+
use IteratorAggregate;
9+
use Traversable;
10+
11+
final class IterableObject implements IteratorAggregate
12+
{
13+
/**
14+
* @var array|Traversable
15+
*/
16+
private $iterable;
17+
18+
/**
19+
* @var callable
20+
*/
21+
private $filter;
22+
23+
/**
24+
* @var callable
25+
*/
26+
private $map;
27+
28+
/**
29+
* IterableObject constructor.
30+
* @param $iterable
31+
* @param callable|null $filter
32+
* @param callable|null $map
33+
* @throws InvalidArgumentException
34+
*/
35+
public function __construct($iterable, $filter = null, $map = null)
36+
{
37+
if (null === $iterable) {
38+
$iterable = new EmptyIterator();
39+
}
40+
if (!is_iterable($iterable)) {
41+
throw new InvalidArgumentException(
42+
sprintf('Expected array or Traversable, got %s', is_object($iterable) ? get_class($iterable) : gettype($iterable))
43+
);
44+
}
45+
46+
// Cannot rely on callable type-hint on PHP 5.3
47+
if (null !== $filter && !is_callable($filter) && !$filter instanceof Closure) {
48+
throw new InvalidArgumentException(
49+
sprintf('Expected callable, got %s', is_object($filter) ? get_class($filter) : gettype($filter))
50+
);
51+
}
52+
53+
if (null !== $map && !is_callable($map) && !$map instanceof Closure) {
54+
throw new InvalidArgumentException(
55+
sprintf('Expected callable, got %s', is_object($map) ? get_class($map) : gettype($map))
56+
);
57+
}
58+
59+
$this->iterable = $iterable;
60+
$this->filter = $filter;
61+
$this->map = $map;
62+
}
63+
64+
/**
65+
* @param callable $filter
66+
* @return self
67+
*/
68+
public function withFilter($filter)
69+
{
70+
return new self($this->iterable, $filter, $this->map);
71+
}
72+
73+
/**
74+
* @param callable $map
75+
* @return self
76+
*/
77+
public function withMap($map)
78+
{
79+
return new self($this->iterable, $this->filter, $map);
80+
}
81+
82+
/**
83+
* @inheritdoc
84+
*/
85+
public function getIterator()
86+
{
87+
$iterable = $this->iterable;
88+
if (null !== $this->filter) {
89+
$iterable = iterable_filter($iterable, $this->filter);
90+
}
91+
if (null !== $this->map) {
92+
$iterable = iterable_map($iterable, $this->map);
93+
}
94+
return iterable_to_traversable($iterable);
95+
}
96+
97+
/**
98+
* @return array
99+
*/
100+
public function asArray()
101+
{
102+
$iterable = $this->iterable;
103+
if (null !== $this->filter) {
104+
$iterable = iterable_filter($iterable, $this->filter);
105+
}
106+
if (null !== $this->map) {
107+
$iterable = iterable_map($iterable, $this->map);
108+
}
109+
return iterable_to_array($iterable);
110+
}
111+
}

src/iterable-functions.php

+63
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<?php
22

3+
use BenTools\IterableFunctions\IterableObject;
4+
5+
if (version_compare(PHP_VERSION, '5.5') >= 0) {
6+
include_once __DIR__ . '/iterable-map-php55.php';
7+
} else {
8+
include_once __DIR__ . '/iterable-map-php53.php';
9+
}
10+
311
if (!function_exists('is_iterable')) {
412

513
/**
@@ -52,3 +60,58 @@ function iterable_to_traversable($iterable)
5260
}
5361
}
5462
}
63+
64+
if (!function_exists('iterable_filter')) {
65+
66+
/**
67+
* Filters an iterable.
68+
*
69+
* @param $iterable
70+
* @param callable $filter
71+
* @return array|CallbackFilterIterator
72+
* @throws InvalidArgumentException
73+
*/
74+
function iterable_filter($iterable, $filter = null)
75+
{
76+
if (!is_iterable($iterable)) {
77+
throw new \InvalidArgumentException(
78+
sprintf('Expected array or Traversable, got %s', is_object($iterable) ? get_class($iterable) : gettype($iterable))
79+
);
80+
}
81+
82+
// Cannot rely on callable type-hint on PHP 5.3
83+
if (null !== $filter && !is_callable($filter) && !$filter instanceof Closure) {
84+
throw new InvalidArgumentException(
85+
sprintf('Expected callable, got %s', is_object($filter) ? get_class($filter) : gettype($filter))
86+
);
87+
}
88+
89+
if (null === $filter) {
90+
$filter = function ($value) {
91+
return (bool) $value;
92+
};
93+
}
94+
95+
if ($iterable instanceof Traversable) {
96+
if (!class_exists('CallbackFilterIterator')) {
97+
throw new \RuntimeException('Class CallbackFilterIterator not found. Try using a polyfill, like symfony/polyfill-php54');
98+
}
99+
return new CallbackFilterIterator(new IteratorIterator($iterable), $filter);
100+
}
101+
102+
return array_filter($iterable, $filter);
103+
}
104+
105+
}
106+
107+
/**
108+
* @param $iterable
109+
* @param callable|null $filter
110+
* @param callable|null $map
111+
* @return Traversable|IterableObject
112+
* @throws InvalidArgumentException
113+
*/
114+
function iterable($iterable, $filter = null, $map = null)
115+
{
116+
return new IterableObject($iterable, $filter, $map);
117+
}

0 commit comments

Comments
 (0)