From 7c899dc3fbb9a92ea587da520a8e0cc9770ff59c Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Thu, 19 Feb 2026 15:05:49 +0530 Subject: [PATCH 1/5] Add chunkArray() method --- system/BaseModel.php | 15 +++++++++++++++ system/Model.php | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/system/BaseModel.php b/system/BaseModel.php index ee3d1a859955..03ce2507f132 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -585,6 +585,21 @@ abstract public function countAllResults(bool $reset = true, bool $test = false) */ abstract public function chunk(int $size, Closure $userFunc); + /** + * Loops over records in batches, allowing you to operate on each chunk at a time. + * This method works only with DB calls. + * + * This method calls the `$userFunc` with the chunk, instead of a single record as in `chunk()`. + * This allows you to operate on multiple records at once, which can be more efficient for certain operations. + * + * @param Closure(array>|array): mixed $userFunc + * + * @return void + * + * @throws DataException + */ + abstract public function chunkArray(int $size, Closure $userFunc); + /** * Fetches the row of database. * diff --git a/system/Model.php b/system/Model.php index dc3e4db94db2..b4d56a999ad4 100644 --- a/system/Model.php +++ b/system/Model.php @@ -560,6 +560,40 @@ public function chunk(int $size, Closure $userFunc) } } + /** + * {@inheritDoc} + * + * Works with `$this->builder` to get the Compiled select to + * determine the rows to operate on. + * This method works only with dbCalls. + */ + public function chunkArray(int $size, Closure $userFunc) + { + $total = $this->builder()->countAllResults(false); + $offset = 0; + + while ($offset <= $total) { + $builder = clone $this->builder(); + $rows = $builder->get($size, $offset); + + if (! $rows) { + throw DataException::forEmptyDataset('chunk'); + } + + $rows = $rows->getResult($this->tempReturnType); + + $offset += $size; + + if ($rows === []) { + continue; + } + + if ($userFunc($rows) === false) { + return; + } + } + } + /** * Provides a shared instance of the Query Builder. * From ffa43950b356552f2c7c2c47d4ad13fc6cc47bcd Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Thu, 19 Feb 2026 15:05:57 +0530 Subject: [PATCH 2/5] Added tests --- tests/system/Models/MiscellaneousModelTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/system/Models/MiscellaneousModelTest.php b/tests/system/Models/MiscellaneousModelTest.php index 556c1b441c18..fd687d5528a7 100644 --- a/tests/system/Models/MiscellaneousModelTest.php +++ b/tests/system/Models/MiscellaneousModelTest.php @@ -39,6 +39,20 @@ public function testChunk(): void $this->assertSame(4, $rowCount); } + public function testChunkArray(): void + { + $chunkCount = 0; + $numRowsInChunk = []; + + $this->createModel(UserModel::class)->chunkArray(2, static function ($rows) use (&$chunkCount, &$numRowsInChunk): void { + $chunkCount++; + $numRowsInChunk[] = count($rows); + }); + + $this->assertSame(2, $chunkCount); + $this->assertSame([2, 2], $numRowsInChunk); + } + public function testCanCreateAndSaveEntityClasses(): void { $entity = $this->createModel(EntityModel::class)->where('name', 'Developer')->first(); From 9d02d4ecab0e2efe75fa4bc34ab164206cc0fe76 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Thu, 19 Feb 2026 15:06:06 +0530 Subject: [PATCH 3/5] Add docs --- user_guide_src/source/changelogs/v4.8.0.rst | 2 ++ user_guide_src/source/models/model.rst | 4 ++++ user_guide_src/source/models/model/064.php | 6 ++++++ 3 files changed, 12 insertions(+) create mode 100644 user_guide_src/source/models/model/064.php diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 026744fbe8f1..31c85cbdb21b 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -56,6 +56,8 @@ Others Model ===== +- Added new ``chunkArray()`` method to ``CodeIgniter\Model`` for processing large datasets in smaller chunks. See :ref:`model-chunk-array` for usage. + Libraries ========= diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index f1383b45f803..822d9ca60bc1 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -908,6 +908,10 @@ This is best used during cronjobs, data exports, or other large tasks. .. literalinclude:: model/049.php +On the other hand, if you want entire chunk to be passed to the Closure, you can use the chunkArray() method. + +.. literalinclude:: model/064.php + .. _model-events-callbacks: Working with Query Builder diff --git a/user_guide_src/source/models/model/064.php b/user_guide_src/source/models/model/064.php new file mode 100644 index 000000000000..94a8e185cc0a --- /dev/null +++ b/user_guide_src/source/models/model/064.php @@ -0,0 +1,6 @@ +chunkArray(100, static function ($rows) { + // do something. + // $rows is an array of rows representing chunk of 100 items. +}); From a9b837147148acea6c874e812de789f4396fc8fb Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Thu, 19 Feb 2026 15:08:10 +0530 Subject: [PATCH 4/5] cs-fix --- system/BaseModel.php | 2 +- tests/system/Models/MiscellaneousModelTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index 03ce2507f132..d69beafddd69 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -592,7 +592,7 @@ abstract public function chunk(int $size, Closure $userFunc); * This method calls the `$userFunc` with the chunk, instead of a single record as in `chunk()`. * This allows you to operate on multiple records at once, which can be more efficient for certain operations. * - * @param Closure(array>|array): mixed $userFunc + * @param Closure(list>|list): mixed $userFunc * * @return void * diff --git a/tests/system/Models/MiscellaneousModelTest.php b/tests/system/Models/MiscellaneousModelTest.php index fd687d5528a7..9c1daff6daac 100644 --- a/tests/system/Models/MiscellaneousModelTest.php +++ b/tests/system/Models/MiscellaneousModelTest.php @@ -41,7 +41,7 @@ public function testChunk(): void public function testChunkArray(): void { - $chunkCount = 0; + $chunkCount = 0; $numRowsInChunk = []; $this->createModel(UserModel::class)->chunkArray(2, static function ($rows) use (&$chunkCount, &$numRowsInChunk): void { From fd694575b413fe0954cb15a77b8aa5f982a9ed16 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Thu, 19 Feb 2026 15:27:57 +0530 Subject: [PATCH 5/5] Some optimizations and added more tests --- system/Model.php | 7 ++- .../system/Models/MiscellaneousModelTest.php | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/system/Model.php b/system/Model.php index b4d56a999ad4..257a1edcf786 100644 --- a/system/Model.php +++ b/system/Model.php @@ -21,6 +21,7 @@ use CodeIgniter\Database\Exceptions\DataException; use CodeIgniter\Entity\Entity; use CodeIgniter\Exceptions\BadMethodCallException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\ModelException; use CodeIgniter\Validation\ValidationInterface; use Config\Database; @@ -569,10 +570,14 @@ public function chunk(int $size, Closure $userFunc) */ public function chunkArray(int $size, Closure $userFunc) { + if ($size <= 0) { + throw new InvalidArgumentException('chunkArray() requires a positive integer for the $size argument.'); + } + $total = $this->builder()->countAllResults(false); $offset = 0; - while ($offset <= $total) { + while ($offset < $total) { $builder = clone $this->builder(); $rows = $builder->get($size, $offset); diff --git a/tests/system/Models/MiscellaneousModelTest.php b/tests/system/Models/MiscellaneousModelTest.php index 9c1daff6daac..a8e438980285 100644 --- a/tests/system/Models/MiscellaneousModelTest.php +++ b/tests/system/Models/MiscellaneousModelTest.php @@ -14,6 +14,8 @@ namespace CodeIgniter\Models; use CodeIgniter\Database\Exceptions\DataException; +use CodeIgniter\Events\Events; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\I18n\Time; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Models\EntityModel; @@ -53,6 +55,62 @@ public function testChunkArray(): void $this->assertSame([2, 2], $numRowsInChunk); } + public function testChunkArrayThrowsOnZeroSize(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('chunkArray() requires a positive integer for the $size argument.'); + + $this->createModel(UserModel::class)->chunkArray(0, static function ($row): void {}); + } + + public function testChunkThrowsOnNegativeSize(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('chunkArray() requires a positive integer for the $size argument.'); + + $this->createModel(UserModel::class)->chunkArray(-1, static function ($row): void {}); + } + + public function testChunkArrayEarlyExit(): void + { + $rowCount = 0; + + $this->createModel(UserModel::class)->chunkArray(2, static function ($rows) use (&$rowCount): bool { + $rowCount++; + + return false; + }); + + $this->assertSame(1, $rowCount); + } + + public function testChunkArrayDoesNotRunExtraQuery(): void + { + $queryCount = 0; + $listener = static function () use (&$queryCount): void { + $queryCount++; + }; + + Events::on('DBQuery', $listener); + $this->createModel(UserModel::class)->chunkArray(4, static function ($rows): void {}); + Events::removeListener('DBQuery', $listener); + + $this->assertSame(2, $queryCount); + } + + public function testChunkArrayEmptyTable(): void + { + $this->db->table('user')->truncate(); + + $rowCount = 0; + + $this->createModel(UserModel::class)->chunkArray(2, static function ($row) use (&$rowCount): void { + $rowCount++; + }); + + $this->assertSame(0, $rowCount); + } + public function testCanCreateAndSaveEntityClasses(): void { $entity = $this->createModel(EntityModel::class)->where('name', 'Developer')->first();