Skip to content

Commit 93affe1

Browse files
author
David Bohn
committed
Initial commit
0 parents  commit 93affe1

File tree

13 files changed

+462
-0
lines changed

13 files changed

+462
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor
2+
composer.lock
3+
.idea
4+
.phpunit.result.cache

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 David Bohn
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.

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Laravel Lazy Chunk
2+
3+
This library adds the ability to work with chunked query results using lazy collections.
4+
5+
## Motivation
6+
7+
Since Laravel 6, there are Lazy Collections, which are a nice way to use the Collection interface on generated data.
8+
9+
One major use case for these were the database query cursors. This allows to work with a query result, as if all results were fetched,
10+
but with one query and small memory footprint.
11+
12+
The issue with these cursors is, that eager loading relationships does not work.
13+
As there is always just one result at hand, eager loads are resolved with one query each.
14+
15+
So if you are working with large data sets that are using relationships, you are still stuck with `chunk()` and `chunkById()`.
16+
17+
Sadly these are still closure methods, which this library changes.
18+
19+
## Usage
20+
21+
Using this library, you gain two new query builder methods:
22+
23+
```php
24+
Builder::lazyChunk($count, callable $callback = null): LazyCollection;
25+
```
26+
27+
and
28+
29+
```php
30+
Builder::lazyChunkById($count, callable $callback = null, $column = null, $alias = null): LazyCollection;
31+
```
32+
33+
The main difference is, that both methods are now returning lazy collections that resolve the chunks. The callback is now optional.
34+
You can do this now:
35+
36+
```php
37+
Article::with('author')->lazyChunk($chunkSize)->flatten(1)->map([$this, 'handleEachArticle']);
38+
```
39+
40+
You can work with the articles there, as if you would have fetched those all in one query with the benefit of the eager loading.
41+
But using the chunking, we are not loading all results at once and thus holding memory usage low.
42+
In fact, the use case of flattening the chunks again to get the elements themselves, is seen as the main use case. Because of this,
43+
the library offers the `flatLazyChunk` alias:
44+
45+
```php
46+
Article::with('author')->flatLazyChunk($chunkSize)->map([$this, 'handleEachArticle']);
47+
```

composer.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "davidbohn/laravel-lazychunk",
3+
"require": {
4+
"php": ">=7.2",
5+
"illuminate/support": "^6.14",
6+
"illuminate/database": "^6.14"
7+
},
8+
"require-dev": {
9+
"orchestra/testbench": "^4.6",
10+
"phpunit/phpunit": "^8.5"
11+
},
12+
"autoload": {
13+
"psr-4": {
14+
"Dababo\\LazyChunk\\": "src"
15+
}
16+
},
17+
"autoload-dev": {
18+
"psr-4": {
19+
"Dababo\\LazyChunk\\Tests\\": "tests"
20+
}
21+
},
22+
"license": "MIT",
23+
"authors": [
24+
{
25+
"name": "David Bohn",
26+
"email": "davbohn@googlemail.com"
27+
}
28+
],
29+
"extra": {
30+
"laravel": {
31+
"providers": [
32+
"Dababo\\LazyChunk\\LazyChunkServiceProvider"
33+
]
34+
}
35+
}
36+
}

phpunit.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit bootstrap="vendor/autoload.php"
3+
backupGlobals="false"
4+
backupStaticAttributes="false"
5+
colors="true"
6+
verbose="true"
7+
convertErrorsToExceptions="true"
8+
convertNoticesToExceptions="true"
9+
convertWarningsToExceptions="true"
10+
processIsolation="false"
11+
stopOnFailure="false">
12+
<testsuites>
13+
<testsuite name="Versioning Test Suite">
14+
<directory>tests</directory>
15+
</testsuite>
16+
</testsuites>
17+
<filter>
18+
<whitelist>
19+
<directory suffix=".php">src/</directory>
20+
</whitelist>
21+
</filter>
22+
</phpunit>

src/LazyChunkMixin.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
4+
namespace Dababo\LazyChunk;
5+
6+
7+
use Illuminate\Support\LazyCollection;
8+
9+
class LazyChunkMixin
10+
{
11+
public function lazyChunk()
12+
{
13+
return function ($count, callable $callback = null) {
14+
return new LazyCollection(function () use ($count, $callback) {
15+
16+
$this->enforceOrderBy();
17+
18+
$page = 1;
19+
20+
do {
21+
// We'll execute the query for the given page and get the results. If there are
22+
// no results we can just break and return from here. When there are results
23+
// we will call the callback with the current chunk of these results here.
24+
$results = $this->forPage($page, $count)->get();
25+
26+
$countResults = $results->count();
27+
28+
if ($countResults == 0) {
29+
break;
30+
}
31+
32+
yield $page => $results;
33+
34+
if ($callback !== null && $callback($results, $page) === false) {
35+
break;
36+
}
37+
38+
unset($results);
39+
40+
$page++;
41+
} while ($countResults == $count);
42+
});
43+
};
44+
}
45+
46+
public function flatLazyChunk()
47+
{
48+
return function ($count, callable $callback = null) {
49+
return $this->lazyChunk($count, $callback)->flatten(1);
50+
};
51+
}
52+
53+
public function lazyChunkById()
54+
{
55+
return function ($count, callable $callback = null, $column = null, $alias = null) {
56+
return new LazyCollection(function () use ($count, $callback, $column, $alias) {
57+
$column = $column ?? $this->defaultKeyName();
58+
59+
$alias = $alias ?? $column;
60+
61+
$lastId = null;
62+
63+
do {
64+
$clone = clone $this;
65+
66+
// We'll execute the query for the given page and get the results. If there are
67+
// no results we can just break and return from here. When there are results
68+
// we will call the callback with the current chunk of these results here.
69+
$results = $clone->forPageAfterId($count, $lastId, $column)->get();
70+
71+
$countResults = $results->count();
72+
73+
if ($countResults == 0) {
74+
break;
75+
}
76+
77+
yield $results;
78+
79+
// On each chunk result set, we will pass them to the callback and then let the
80+
// developer take care of everything within the callback, which allows us to
81+
// keep the memory low for spinning through large result sets for working.
82+
if ($callback !== null && $callback($results) === false) {
83+
return false;
84+
}
85+
86+
$lastId = $results->last()->{$alias};
87+
88+
unset($results);
89+
} while ($countResults == $count);
90+
});
91+
};
92+
}
93+
94+
public function flatLazyChunkById()
95+
{
96+
return function ($count, callable $callable = null, $column = null, $alias = null) {
97+
return $this->lazyChunkById($count, $callable, $column, $alias)->flatten(1);
98+
};
99+
}
100+
}

src/LazyChunkServiceProvider.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
4+
namespace Dababo\LazyChunk;
5+
6+
7+
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
8+
use Illuminate\Database\Query\Builder;
9+
use Illuminate\Support\ServiceProvider;
10+
11+
class LazyChunkServiceProvider extends ServiceProvider
12+
{
13+
public function register()
14+
{
15+
Builder::mixin(new LazyChunkMixin());
16+
17+
EloquentBuilder::mixin(new LazyChunkMixin());
18+
}
19+
}

tests/Helpers/MockCallback.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
4+
namespace Dababo\LazyChunk\Tests\Helpers;
5+
6+
7+
8+
use PHPUnit\Framework\Assert;
9+
10+
class MockCallback {
11+
protected $invocations = 0;
12+
13+
protected $finalInvocations = null;
14+
15+
public static function mustBeCalled($times = 1, $action = null)
16+
{
17+
return (new static())->willBeCalled($times)->getCallable($action);
18+
}
19+
20+
public function willBeCalled($times)
21+
{
22+
$this->finalInvocations = $times;
23+
24+
return $this;
25+
}
26+
27+
public function __destruct()
28+
{
29+
if ($this->finalInvocations !== null) {
30+
$this->assertHasBeenCalled($this->finalInvocations);
31+
}
32+
}
33+
34+
public function getCallable(callable $action = null): callable
35+
{
36+
return function (...$args) use ($action) {
37+
$this->invocations++;
38+
39+
if ($action !== null) {
40+
return $action(...$args);
41+
}
42+
};
43+
}
44+
45+
public function assertHasBeenCalled($invocations = 1)
46+
{
47+
Assert::assertSame($invocations, $this->invocations, "The callback was not called correctly!");
48+
49+
$this->invocations = 0;
50+
}
51+
}

0 commit comments

Comments
 (0)