Skip to content

Commit

Permalink
[11.x] Add IncrementOrCreate method to Eloquent (#54300)
Browse files Browse the repository at this point in the history
* feat: added incrementOrCreate new method

* added increment step and extra params to incrementOrCreate

* StyleCI
  • Loading branch information
carloeusebi authored Jan 22, 2025
1 parent b0667a6 commit b89006c
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 0 deletions.
19 changes: 19 additions & 0 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,25 @@ public function updateOrCreate(array $attributes, array $values = [])
});
}

/**
* Create a record matching the attributes, or increment the existing record.
*
* @param array $attributes
* @param string $column
* @param int|float $default
* @param int|float $step
* @param array $extra
* @return TModel
*/
public function incrementOrCreate(array $attributes, string $column = 'count', $default = 1, $step = 1, array $extra = [])
{
return tap($this->firstOrCreate($attributes, [$column => $default]), function ($instance) use ($column, $step, $extra) {
if (! $instance->wasRecentlyCreated) {
$instance->increment($column, $step, $extra);
}
});
}

/**
* Execute the query and get the first result or throw an exception.
*
Expand Down
169 changes: 169 additions & 0 deletions tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,175 @@ public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void
], $result->toArray());
}

public function testIncrementOrCreateMethodIncrementsExistingRecord(): void
{
$model = new EloquentBuilderCreateOrFirstTestModel();
$this->mockConnectionForModel($model, 'SQLite');
$model->getConnection()->shouldReceive('transactionLevel')->andReturn(0);
$model->getConnection()->shouldReceive('getName')->andReturn('sqlite');

$model->getConnection()
->expects('select')
->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true)
->andReturn([[
'id' => 123,
'attr' => 'foo',
'count' => 1,
'created_at' => '2023-01-01 00:00:00',
'updated_at' => '2023-01-01 00:00:00',
]]);

$model->getConnection()
->expects('raw')
->with('"count" + 1')
->andReturn('2');

$model->getConnection()
->expects('update')
->with(
'update "table" set "count" = ?, "updated_at" = ? where "id" = ?',
['2', '2023-01-01 00:00:00', 123],
)
->andReturn(1);

$result = $model->newQuery()->incrementOrCreate(['attr' => 'foo'], 'count');
$this->assertFalse($result->wasRecentlyCreated);
$this->assertEquals([
'id' => 123,
'attr' => 'foo',
'count' => 2,
'created_at' => '2023-01-01T00:00:00.000000Z',
'updated_at' => '2023-01-01T00:00:00.000000Z',
], $result->toArray());
}

public function testIncrementOrCreateMethodCreatesNewRecord(): void
{
$model = new EloquentBuilderCreateOrFirstTestModel();
$this->mockConnectionForModel($model, 'SQLite', [123]);
$model->getConnection()->shouldReceive('transactionLevel')->andReturn(0);
$model->getConnection()->shouldReceive('getName')->andReturn('sqlite');

$model->getConnection()
->expects('select')
->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true)
->andReturn([]);

$model->getConnection()->expects('insert')->with(
'insert into "table" ("attr", "count", "updated_at", "created_at") values (?, ?, ?, ?)',
['foo', '1', '2023-01-01 00:00:00', '2023-01-01 00:00:00'],
)->andReturnTrue();

$result = $model->newQuery()->incrementOrCreate(['attr' => 'foo']);
$this->assertTrue($result->wasRecentlyCreated);
$this->assertEquals([
'id' => 123,
'attr' => 'foo',
'count' => 1,
'created_at' => '2023-01-01T00:00:00.000000Z',
'updated_at' => '2023-01-01T00:00:00.000000Z',
], $result->toArray());
}

public function testIncrementOrCreateMethodIncrementParametersArePassed(): void
{
$model = new EloquentBuilderCreateOrFirstTestModel();
$this->mockConnectionForModel($model, 'SQLite');
$model->getConnection()->shouldReceive('transactionLevel')->andReturn(0);
$model->getConnection()->shouldReceive('getName')->andReturn('sqlite');

$model->getConnection()
->expects('select')
->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true)
->andReturn([[
'id' => 123,
'attr' => 'foo',
'val' => 'bar',
'count' => 1,
'created_at' => '2023-01-01 00:00:00',
'updated_at' => '2023-01-01 00:00:00',
]]);

$model->getConnection()
->expects('raw')
->with('"count" + 2')
->andReturn('3');

$model->getConnection()
->expects('update')
->with(
'update "table" set "count" = ?, "val" = ?, "updated_at" = ? where "id" = ?',
['3', 'baz', '2023-01-01 00:00:00', 123],
)
->andReturn(1);

$result = $model->newQuery()->incrementOrCreate(['attr' => 'foo'], step: 2, extra: ['val' => 'baz']);
$this->assertFalse($result->wasRecentlyCreated);
$this->assertEquals([
'id' => 123,
'attr' => 'foo',
'count' => 3,
'val' => 'baz',
'created_at' => '2023-01-01T00:00:00.000000Z',
'updated_at' => '2023-01-01T00:00:00.000000Z',
], $result->toArray());
}

public function testIncrementOrCreateMethodRetrievesRecordCreatedJustNow(): void
{
$model = new EloquentBuilderCreateOrFirstTestModel();
$this->mockConnectionForModel($model, 'SQLite');
$model->getConnection()->shouldReceive('transactionLevel')->andReturn(0);
$model->getConnection()->shouldReceive('getName')->andReturn('sqlite');

$model->getConnection()
->expects('select')
->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true)
->andReturn([]);

$sql = 'insert into "table" ("attr", "count", "updated_at", "created_at") values (?, ?, ?, ?)';
$bindings = ['foo', '1', '2023-01-01 00:00:00', '2023-01-01 00:00:00'];

$model->getConnection()
->expects('insert')
->with($sql, $bindings)
->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception()));

$model->getConnection()
->expects('select')
->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], false)
->andReturn([[
'id' => 123,
'attr' => 'foo',
'count' => 1,
'created_at' => '2023-01-01 00:00:00',
'updated_at' => '2023-01-01 00:00:00',
]]);

$model->getConnection()
->expects('raw')
->with('"count" + 1')
->andReturn('2');

$model->getConnection()
->expects('update')
->with(
'update "table" set "count" = ?, "updated_at" = ? where "id" = ?',
['2', '2023-01-01 00:00:00', 123],
)
->andReturn(1);

$result = $model->newQuery()->incrementOrCreate(['attr' => 'foo']);
$this->assertFalse($result->wasRecentlyCreated);
$this->assertEquals([
'id' => 123,
'attr' => 'foo',
'count' => 2,
'created_at' => '2023-01-01T00:00:00.000000Z',
'updated_at' => '2023-01-01T00:00:00.000000Z',
], $result->toArray());
}

protected function mockConnectionForModel(Model $model, string $database, array $lastInsertIds = []): void
{
$grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar';
Expand Down

0 comments on commit b89006c

Please sign in to comment.