diff --git a/system/BaseModel.php b/system/BaseModel.php index ee3d1a859955..d69beafddd69 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(list>|list): 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..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; @@ -560,6 +561,44 @@ 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) + { + 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) { + $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. * diff --git a/tests/system/Models/MiscellaneousModelTest.php b/tests/system/Models/MiscellaneousModelTest.php index 556c1b441c18..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; @@ -39,6 +41,76 @@ 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 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(); 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. +});