Skip to content

Commit f0a2e3c

Browse files
committed
Enhance README with detailed Flow usage examples and clarify iterator behavior. Update test runner to improve failure reporting and results tracking.
Configured GitHub Actions to run the suite on master pushes.
1 parent ff4797d commit f0a2e3c

File tree

4 files changed

+97
-25
lines changed

4 files changed

+97
-25
lines changed

.github/workflows/tests.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
8+
jobs:
9+
run-tests:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout repository
13+
uses: actions/checkout@v4
14+
15+
- name: Set up PHP
16+
uses: shivammathur/setup-php@v2
17+
with:
18+
php-version: '8.1'
19+
coverage: none
20+
tools: composer
21+
22+
- name: Install dependencies
23+
run: composer install --no-interaction --no-progress --prefer-dist
24+
25+
- name: Run test suite
26+
run: php tests/FlowTest.php

README.md

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,34 @@ The same workflow using temporary arrays and for-loops would be longer, harder t
3535
memory multiple times. A Flow pipeline keeps the computation streaming, lets you re-use SPL iterators seamlessly and
3636
makes experimentation as simple as inserting or removing a step.
3737

38+
## Flow is **not** the usual Collection-style utility library
39+
40+
Typical Collection-like classes use arrays underneath. But `Flow`, even though it also uses a chainable fluent interface, is both an `Iterator` and a `Traversable` (therefore with native PHP support), and works perfectly with **Generators** and **SPL iterators**, with no arrays underneath. **Each iteration step only requires one value to be in memory (it can be generated on the fly), and the data is streamed through the pipeline**.
41+
42+
Since Flow implements both `Iterator` and `Traversable`, you can use a Flow object anywhere you would use a `Traversable` or `Iterator`. For example, you can use `foreach` directly over a Flow object:
43+
44+
```php
45+
foreach (Flow::from([1, 2, 3]) as $value) {
46+
echo $value;
47+
}
48+
```
49+
50+
You can also pass Flow objects to any function that expects an `Iterator` or `Traversable`:
51+
52+
```php
53+
// Flow works seamlessly with SPL functions
54+
$count = iterator_count(Flow::from([1, 2, 3])->where(fn($x) => $x > 1));
55+
56+
// Or with any function expecting a Traversable
57+
function processItems(Traversable $items) {
58+
foreach ($items as $item) {
59+
// process each item
60+
}
61+
}
62+
63+
processItems(Flow::from($data)->map($transformer));
64+
```
65+
3866
## Fluent operations reference
3967

4068
### Creating flows
@@ -71,12 +99,12 @@ makes experimentation as simple as inserting or removing a step.
7199
| `mapAndFilter(callable $fn, $arg = null)` | Combines mapping and filtering: return a value to keep it, or `null` to drop it.
72100
| `flip()` | Swaps keys with their corresponding values while iterating.
73101
| `keys()` | Replaces each value by its key and reindexes the keys sequentially.
74-
| `reindex(int $start = 0, int $step = 1)` | Rewrites keys as a numeric sequence without materialising the data.
102+
| `reindex(int $start = 0, int $step = 1)` | Rewrites keys as a numeric sequence without materializing the data.
75103
| `regex(string $pattern, int $flags = 0, bool $useKeys = false)` | Replaces each item with the full set of regular expression matches.
76104
| `regexExtract(string $pattern, int $flags = 0, bool $useKeys = false)` | Extracts the first regex match for each item.
77-
| `regexMap(string $pattern, string $replacement, bool $useKeys = false)` | Performs regex replacements on the fly using `RegexIterator::setReplacement()`.
105+
| `regexMap(string $pattern, string $replaceWith, bool $useKeys = false)` | Performs regex replacements on the fly using `RegexIterator::setReplacement()`.
78106
| `regexSplit(string $pattern, int $flags = 0, bool $useKeys = false)` | Splits strings by regex and yields the resulting fragments.
79-
| `swap(callable $fn)` | Materialises the stream, lets a callback replace the dataset, then restarts iteration.
107+
| `swap(callable $fn)` | Materializes the stream, lets a callback replace the dataset, then restarts iteration.
80108
| `reduce(callable $fn, $seedValue = null)` | Collapses the flow into a single value by folding with an accumulator.
81109

82110
### Filtering, gating and flow control
@@ -91,18 +119,18 @@ makes experimentation as simple as inserting or removing a step.
91119
| `repeatWhile(callable $fn)` | Replays the flow until the callback tells it to stop.
92120
| `only(int $n)` | Limits the flow to the first *n* items, regardless of key types.
93121
| `skip(int $n = 1)` | Skips the first *n* items and continues streaming.
94-
| `drop(int $n = 1)` | Removes the last *n* items (materialises and trims the array).
122+
| `drop(int $n = 1)` | Removes the last *n* items (materializes and trims the array).
95123
| `slice(int $offset = 0, int $count = -1)` | Delegates to `LimitIterator` to take a window of items.
96124
| `noRewind()` | Wraps the iterator with `NoRewindIterator` so it cannot be rewound after the first traversal.
97125

98-
### Ordering, caching and materialisation
126+
### Ordering, caching and materialization
99127

100128
| Method | Description |
101129
| --- | --- |
102-
| `sort(string $type = 'sort', int $flags = SORT_REGULAR, ?callable $fn = null)` | Materialises and delegates to the native PHP sort family, preserving keys where appropriate.
103-
| `reverse(bool $preserveKeys = false)` | Materialises, reverses and exposes the sequence through a generator.
104-
| `cache()` | Memoises the iterator so future traversals re-use cached values.
105-
| `pack()` | Materialises and converts the flow into a zero-based array while retaining order.
130+
| `sort(string $type = 'sort', int $flags = SORT_REGULAR, ?callable $fn = null)` | Materializes and delegates to the native PHP sort family, preserving keys where appropriate.
131+
| `reverse(bool $preserveKeys = false)` | Materializes, reverses and exposes the sequence through a generator.
132+
| `cache()` | Memoizes the iterator so future traversals re-use cached values.
133+
| `pack()` | Materializes and converts the flow into a zero-based array while retaining order.
106134
| `all()` | Collects the entire dataset into an array, preserving keys.
107135

108136
### Inspecting and manual iteration helpers
@@ -112,7 +140,7 @@ makes experimentation as simple as inserting or removing a step.
112140
| `fetch()` | Reads and advances a single value from the flow (auto-rewinds on first call).
113141
| `fetchKey()` | Reads and advances a single key from the flow.
114142
| `current(): mixed`, `key(): mixed`, `next(): void`, `rewind(): void`, `valid(): bool` | Implement the native `Iterator` interface so you can loop over `Flow` directly.
115-
| `getIterator(): Iterator` | Returns the current iterator (materialising data if needed).
143+
| `getIterator(): Iterator` | Returns the current iterator (materializing data if needed).
116144
| `setIterator($iterable)` | Replaces the underlying iterator; intended for advanced scenarios.
117145

118146
### Filesystem flows
@@ -123,7 +151,7 @@ makes experimentation as simple as inserting or removing a step.
123151
| --- | --- |
124152
| `FilesystemFlow::from(string $path, int $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS)` | Streams directory entries with full control over SPL flags.
125153
| `FilesystemFlow::glob(string $pattern, int $flags = 0)` | Iterates filesystem matches similar to `glob()` but lazily.
126-
| `FilesystemFlow::recursiveFrom(string $path, int $flags = default, int $mode = FilesystemFlow::DIRECTORIES_FIRST)` | Builds a recursive traversal using `RecursiveIteratorIterator` and a configurable recursion mode.
154+
| `FilesystemFlow::recursiveFrom(string $path, int $flags = FilesystemIterator::KEY_AS_PATHNAME \| FilesystemIterator::CURRENT_AS_FILEINFO \| FilesystemIterator::SKIP_DOTS, int $mode = RecursiveIteratorIterator::SELF_FIRST)` | Builds a recursive traversal using `RecursiveIteratorIterator` and a configurable recursion mode.
127155
| `FilesystemFlow::recursiveGlob(string $rootDir, string $pattern, int $flags = 0)` | Performs recursive glob searches, yielding `SplFileInfo` objects or paths according to flags.
128156
| `onlyDirectories()` | Filters to directory entries only (requires `CURRENT_AS_FILEINFO`).
129157
| `onlyFiles()` | Filters to file entries only (requires `CURRENT_AS_FILEINFO`).
@@ -134,7 +162,7 @@ Flow ships with a set of custom iterators that integrate seamlessly with SPL:
134162

135163
| Iterator | Purpose |
136164
| --- | --- |
137-
| `CachedIterator` | *Memoises* another iterator so that subsequent traversals reuse cached values.
165+
| `CachedIterator` | *Memoizes* another iterator so that subsequent traversals reuse cached values.
138166
| `ConditionalIterator` | Iterates until a callback vetoes further processing.
139167
| `FlipIterator` | Swaps keys for values (and optionally values for keys) while delegating iteration.
140168
| `FunctionIterator` | Creates generators from callbacks so you can yield values without native generators.
@@ -162,7 +190,7 @@ The `globals.php` helpers make it effortless to adopt Flow throughout an applica
162190

163191
## Notes
164192

165-
Some operations (such as `reverse()` or `sort()`) need to materialise the stream into an array before continuing. The
193+
Some operations (such as `reverse()` or `sort()`) need to materialize the stream into an array before continuing. The
166194
library only buffers data when absolutely necessary and automatically returns to streaming mode afterwards.
167195

168196
## License

tests/FlowTest.php

100644100755
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#!/usr/bin/env php
12
<?php
23
require __DIR__ . '/bootstrap.php';
34
require __DIR__ . '/TestRunner.php';
@@ -938,7 +939,7 @@ public function getIterator(): Traversable
938939
$withEmpty->nextOuter();
939940
$tests->truthy($withEmpty->valid());
940941
$tests->same('value', $withEmpty->current());
941-
$tests->same([0 => 1, 'a' => 2, 'b' => 3, 2 => 4], iterator_to_array($iterator));
942+
$tests->same([0 => 'value', 1 => 7], iterator_to_array($withEmpty));
942943
});
943944

944945
exit($tests->report());

tests/TestRunner.php

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,22 @@
33
class TestRunner
44
{
55
private int $tests = 0;
6-
/** @var array<int, array{string, Throwable}> */
7-
private array $failures = [];
6+
/** @var array<int, array{int, string, Throwable|null}> */
7+
private array $results = [];
88

99
/**
1010
* @param callable():void $test
1111
*/
1212
public function it(string $description, callable $test): void
1313
{
14-
$this->tests++;
14+
$index = ++$this->tests;
1515
try {
1616
$test();
17-
echo '.';
17+
$this->results[] = [$index, $description, null];
18+
$this->printResult($index, '', $description);
1819
} catch (Throwable $e) {
19-
$this->failures[] = [$description, $e];
20-
echo 'F';
20+
$this->results[] = [$index, $description, $e];
21+
$this->printResult($index, '', $description);
2122
}
2223
}
2324

@@ -48,17 +49,33 @@ public function truthy(mixed $value, string $message = ''): void
4849

4950
public function report(): int
5051
{
51-
echo PHP_EOL . PHP_EOL;
52-
if ($this->failures === []) {
52+
echo PHP_EOL;
53+
$failures = array_filter(
54+
$this->results,
55+
static fn (array $result): bool => $result[2] instanceof Throwable
56+
);
57+
58+
if ($failures === []) {
5359
printf("%d tests passed.\n", $this->tests);
5460
return 0;
5561
}
5662

57-
printf("%d of %d tests failed:\n", count($this->failures), $this->tests);
58-
foreach ($this->failures as [$description, $error]) {
59-
printf("- %s: %s\n", $description, $error->getMessage());
63+
printf("%d of %d tests failed:\n", count($failures), $this->tests);
64+
foreach ($failures as [$index, $description, $error]) {
65+
printf("- %s. %s: %s\n", $this->formatIndex($index), $description, $error->getMessage());
6066
}
6167

6268
return 1;
6369
}
70+
71+
private function printResult(int $index, string $symbol, string $description): void
72+
{
73+
printf("%s. %s %s\n", $this->formatIndex($index), $symbol, $description);
74+
}
75+
76+
private function formatIndex(int $index): string
77+
{
78+
$digits = max(2, strlen((string) $index));
79+
return str_pad((string) $index, $digits, '0', STR_PAD_LEFT);
80+
}
6481
}

0 commit comments

Comments
 (0)