Skip to content

Commit 91ee57b

Browse files
authored
feat: add chunkRows() method in models (#9962)
1 parent cd3013b commit 91ee57b

File tree

6 files changed

+141
-11
lines changed

6 files changed

+141
-11
lines changed

system/BaseModel.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,22 @@ abstract public function countAllResults(bool $reset = true, bool $test = false)
586586
*/
587587
abstract public function chunk(int $size, Closure $userFunc);
588588

589+
/**
590+
* Loops over records in batches, allowing you to operate on each chunk at a time.
591+
* This method works only with DB calls.
592+
*
593+
* This method calls the `$userFunc` with the chunk, instead of a single record as in `chunk()`.
594+
* This allows you to operate on multiple records at once, which can be more efficient for certain operations.
595+
*
596+
* @param Closure(list<array<string, string>>|list<object>): mixed $userFunc
597+
*
598+
* @return void
599+
*
600+
* @throws DataException
601+
* @throws InvalidArgumentException if $size is not a positive integer
602+
*/
603+
abstract public function chunkRows(int $size, Closure $userFunc);
604+
589605
/**
590606
* Fetches the row of database.
591607
*

system/Model.php

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use CodeIgniter\Validation\ValidationInterface;
2727
use Config\Database;
2828
use Config\Feature;
29+
use Generator;
2930
use stdClass;
3031

3132
/**
@@ -526,16 +527,16 @@ public function countAllResults(bool $reset = true, bool $test = false)
526527
}
527528

528529
/**
529-
* {@inheritDoc}
530+
* Iterates over the result set in chunks of the specified size.
530531
*
531-
* Works with `$this->builder` to get the Compiled select to
532-
* determine the rows to operate on.
533-
* This method works only with dbCalls.
532+
* @param int $size The number of records to retrieve in each chunk.
533+
*
534+
* @return Generator<list<array<string, string>>|list<object>>
534535
*/
535-
public function chunk(int $size, Closure $userFunc)
536+
private function iterateChunks(int $size): Generator
536537
{
537538
if ($size <= 0) {
538-
throw new InvalidArgumentException('chunk() requires a positive integer for the $size argument.');
539+
throw new InvalidArgumentException('$size must be a positive integer.');
539540
}
540541

541542
$total = $this->builder()->countAllResults(false);
@@ -557,6 +558,16 @@ public function chunk(int $size, Closure $userFunc)
557558
continue;
558559
}
559560

561+
yield $rows;
562+
}
563+
}
564+
565+
/**
566+
* {@inheritDoc}
567+
*/
568+
public function chunk(int $size, Closure $userFunc)
569+
{
570+
foreach ($this->iterateChunks($size) as $rows) {
560571
foreach ($rows as $row) {
561572
if ($userFunc($row) === false) {
562573
return;
@@ -565,6 +576,18 @@ public function chunk(int $size, Closure $userFunc)
565576
}
566577
}
567578

579+
/**
580+
* {@inheritDoc}
581+
*/
582+
public function chunkRows(int $size, Closure $userFunc): void
583+
{
584+
foreach ($this->iterateChunks($size) as $rows) {
585+
if ($userFunc($rows) === false) {
586+
return;
587+
}
588+
}
589+
}
590+
568591
/**
569592
* Provides a shared instance of the Query Builder.
570593
*

tests/system/Models/MiscellaneousModelTest.php

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ public function testChunk(): void
4444
public function testChunkThrowsOnZeroSize(): void
4545
{
4646
$this->expectException(InvalidArgumentException::class);
47-
$this->expectExceptionMessage('chunk() requires a positive integer for the $size argument.');
47+
$this->expectExceptionMessage('$size must be a positive integer.');
4848

4949
$this->createModel(UserModel::class)->chunk(0, static function ($row): void {});
5050
}
5151

5252
public function testChunkThrowsOnNegativeSize(): void
5353
{
5454
$this->expectException(InvalidArgumentException::class);
55-
$this->expectExceptionMessage('chunk() requires a positive integer for the $size argument.');
55+
$this->expectExceptionMessage('$size must be a positive integer.');
5656

5757
$this->createModel(UserModel::class)->chunk(-1, static function ($row): void {});
5858
}
@@ -97,6 +97,76 @@ public function testChunkEmptyTable(): void
9797
$this->assertSame(0, $rowCount);
9898
}
9999

100+
public function testChunkRows(): void
101+
{
102+
$chunkCount = 0;
103+
$numRowsInChunk = [];
104+
105+
$this->createModel(UserModel::class)->chunkRows(2, static function ($rows) use (&$chunkCount, &$numRowsInChunk): void {
106+
$chunkCount++;
107+
$numRowsInChunk[] = count($rows);
108+
});
109+
110+
$this->assertSame(2, $chunkCount);
111+
$this->assertSame([2, 2], $numRowsInChunk);
112+
}
113+
114+
public function testChunkRowsThrowsOnZeroSize(): void
115+
{
116+
$this->expectException(InvalidArgumentException::class);
117+
$this->expectExceptionMessage('$size must be a positive integer.');
118+
119+
$this->createModel(UserModel::class)->chunkRows(0, static function ($row): void {});
120+
}
121+
122+
public function testChunkRowsThrowsOnNegativeSize(): void
123+
{
124+
$this->expectException(InvalidArgumentException::class);
125+
$this->expectExceptionMessage('$size must be a positive integer.');
126+
127+
$this->createModel(UserModel::class)->chunkRows(-1, static function ($row): void {});
128+
}
129+
130+
public function testChunkRowsEarlyExit(): void
131+
{
132+
$rowCount = 0;
133+
134+
$this->createModel(UserModel::class)->chunkRows(2, static function ($rows) use (&$rowCount): bool {
135+
$rowCount++;
136+
137+
return false;
138+
});
139+
140+
$this->assertSame(1, $rowCount);
141+
}
142+
143+
public function testChunkRowsDoesNotRunExtraQuery(): void
144+
{
145+
$queryCount = 0;
146+
$listener = static function () use (&$queryCount): void {
147+
$queryCount++;
148+
};
149+
150+
Events::on('DBQuery', $listener);
151+
$this->createModel(UserModel::class)->chunkRows(4, static function ($rows): void {});
152+
Events::removeListener('DBQuery', $listener);
153+
154+
$this->assertSame(2, $queryCount);
155+
}
156+
157+
public function testChunkRowsEmptyTable(): void
158+
{
159+
$this->db->table('user')->truncate();
160+
161+
$rowCount = 0;
162+
163+
$this->createModel(UserModel::class)->chunkRows(2, static function ($row) use (&$rowCount): void {
164+
$rowCount++;
165+
});
166+
167+
$this->assertSame(0, $rowCount);
168+
}
169+
100170
public function testCanCreateAndSaveEntityClasses(): void
101171
{
102172
$entity = $this->createModel(EntityModel::class)->where('name', 'Developer')->first();

user_guide_src/source/changelogs/v4.8.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ Others
127127
Model
128128
=====
129129

130+
- Added new ``chunkRows()`` method to ``CodeIgniter\Model`` for processing large datasets in smaller chunks.
131+
130132
Libraries
131133
=========
132134

user_guide_src/source/models/model.rst

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -900,14 +900,27 @@ Processing Large Amounts of Data
900900
================================
901901

902902
Sometimes, you need to process large amounts of data and would run the risk of running out of memory.
903-
To make this simpler, you may use the chunk() method to get smaller chunks of data that you can then
903+
This is best used during cronjobs, data exports, or other large tasks. To make this simpler, you can
904+
process the data in smaller, manageable pieces using the methods below.
905+
906+
chunk()
907+
-------
908+
909+
You may use the ``chunk()`` method to get smaller chunks of data that you can then
904910
do your work on. The first parameter is the number of rows to retrieve in a single chunk. The second
905911
parameter is a Closure that will be called for each row of data.
906912

907-
This is best used during cronjobs, data exports, or other large tasks.
908-
909913
.. literalinclude:: model/049.php
910914

915+
chunkRows()
916+
-----------
917+
918+
.. versionadded:: 4.8.0
919+
920+
On the other hand, if you want the entire chunk to be passed to the Closure at once, you can use the ``chunkRows()`` method.
921+
922+
.. literalinclude:: model/064.php
923+
911924
.. _model-events-callbacks:
912925

913926
Working with Query Builder
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
$userModel->chunkRows(100, static function ($rows) {
4+
// do something.
5+
// $rows is an array of rows representing chunk of 100 items.
6+
});

0 commit comments

Comments
 (0)