Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions system/BaseModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<array<string, string>>|list<object>): mixed $userFunc
*
* @return void
*
* @throws DataException
*/
abstract public function chunkArray(int $size, Closure $userFunc);

/**
* Fetches the row of database.
*
Expand Down
39 changes: 39 additions & 0 deletions system/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down
72 changes: 72 additions & 0 deletions tests/system/Models/MiscellaneousModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=========

Expand Down
4 changes: 4 additions & 0 deletions user_guide_src/source/models/model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions user_guide_src/source/models/model/064.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

$userModel->chunkArray(100, static function ($rows) {
// do something.
// $rows is an array of rows representing chunk of 100 items.
});
Loading