Skip to content

Commit cd539d8

Browse files
committed
option to useTransactions() for insert and update
1 parent bea240b commit cd539d8

File tree

4 files changed

+215
-6
lines changed

4 files changed

+215
-6
lines changed

src/Traits/HasRelations.php

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace Michalsn\CodeIgniterNestedModel\Traits;
66

77
use Closure;
8+
use CodeIgniter\Database\Exceptions\DatabaseException;
9+
use CodeIgniter\Database\Exceptions\DataException;
810
use CodeIgniter\Model;
911
use LogicException;
1012
use Michalsn\CodeIgniterNestedModel\Enums\RelationTypes;
@@ -18,7 +20,9 @@
1820

1921
trait HasRelations
2022
{
21-
private array $relations = [];
23+
private array $relations = [];
24+
private array $relationErrors = [];
25+
private bool $useTransactions = false;
2226

2327
/**
2428
* Set up model events and initialize
@@ -262,10 +266,17 @@ protected function relationsAfterInsert(array $eventData): array
262266

263267
foreach ($this->relations as $relationObject) {
264268
foreach ($relationObject->getData() as $row) {
265-
$row = $this->transformDataToArray($row, 'insert');
266-
$relationObject->applyWith()->model->insert(array_merge($row, [
269+
$row = $this->transformDataToArray($row, 'insert');
270+
$result = $relationObject->applyWith()->model->insert(array_merge($row, [
267271
$relationObject->foreignKey => $eventData[$this->primaryKey],
268272
]));
273+
274+
if ($result === false) {
275+
$this->relationErrors = array_merge($this->relationErrors, $relationObject->model->errors());
276+
$eventData['result'] = false;
277+
278+
return $eventData;
279+
}
269280
}
270281
}
271282

@@ -309,9 +320,16 @@ protected function relationsAfterUpdate(array $eventData): array
309320

310321
$relationObject->applyConditions();
311322

312-
$query->save(array_merge($row, [
323+
$result = $query->save(array_merge($row, [
313324
$relationObject->foreignKey => $id,
314325
]));
326+
327+
if ($result === false) {
328+
$this->relationErrors = array_merge($this->relationErrors, $relationObject->model->errors());
329+
$eventData['result'] = false;
330+
331+
return $eventData;
332+
}
315333
}
316334
}
317335
}
@@ -404,7 +422,7 @@ protected function getDataForRelationByIds(array $id, Relation $relation): array
404422
}
405423

406424
$relationData = [];
407-
$key = $relation->hasThrough() ? $relation->primaryKey : $relation->foreignKey;
425+
$key = $relation->foreignKey;
408426

409427
if (in_array($relation->type, [RelationTypes::hasOne, RelationTypes::belongsTo], true)) {
410428
foreach ($results as $row) {
@@ -420,4 +438,81 @@ protected function getDataForRelationByIds(array $id, Relation $relation): array
420438

421439
return $relationData;
422440
}
441+
442+
/**
443+
* Whether to use transaction during insert/update.
444+
*/
445+
public function useTransactions(bool $value = true): static
446+
{
447+
$this->useTransactions = $value;
448+
449+
return $this;
450+
}
451+
452+
public function insert($row = null, bool $returnID = true): bool|int|string
453+
{
454+
if ($this->useTransactions) {
455+
try {
456+
$this->db->transException(true)->transStart();
457+
458+
$result = parent::insert($row, $returnID);
459+
460+
if ($this->errors() !== []) {
461+
$this->db->transRollback();
462+
463+
return $result;
464+
}
465+
466+
$this->db->transComplete();
467+
} catch (DatabaseException|DataException $e) {
468+
$this->relationErrors['database_error'] = $e->getMessage();
469+
470+
return false;
471+
} finally {
472+
$this->useTransactions(false);
473+
}
474+
475+
return $result;
476+
}
477+
478+
return parent::insert($row, $returnID);
479+
}
480+
481+
public function update($id = null, $row = null): bool
482+
{
483+
if ($this->useTransactions) {
484+
try {
485+
$this->db->transException(true)->transStart();
486+
487+
$result = parent::update($id, $row);
488+
489+
if ($this->errors() !== []) {
490+
$this->db->transRollback();
491+
492+
return $result;
493+
}
494+
495+
$this->db->transComplete();
496+
} catch (DatabaseException|DataException $e) {
497+
$this->relationErrors['database_error'] = $e->getMessage();
498+
499+
return false;
500+
} finally {
501+
$this->useTransactions(false);
502+
}
503+
504+
return $result;
505+
}
506+
507+
return parent::update($id, $row);
508+
}
509+
510+
public function errors(bool $forceDB = false)
511+
{
512+
if ($this->relationErrors !== []) {
513+
return $this->relationErrors;
514+
}
515+
516+
return parent::errors($forceDB);
517+
}
423518
}

tests/ModelTest.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,115 @@ public function testUpdateHasMany()
145145
]
146146
);
147147
}
148+
149+
public function testInsertValidationErrors()
150+
{
151+
$user = [
152+
'username' => 'Test User',
153+
'company_id' => '1',
154+
'country_id' => '1',
155+
'profile' => [
156+
'country' => 'United States of America and something more to violate the validation rule',
157+
],
158+
];
159+
160+
$userModel = model(UserModel::class);
161+
$userModel->with('profile')->useTransactions()->insert($user);
162+
163+
$this->assertArrayHasKey('country', $userModel->errors());
164+
$this->assertSame('The country field cannot exceed 20 characters in length.', $userModel->errors()['country']);
165+
166+
$this->dontSeeInDatabase(
167+
'users',
168+
[
169+
'id' => '3',
170+
'username' => 'Test User',
171+
]
172+
);
173+
}
174+
175+
public function testUpdateValidationErrors()
176+
{
177+
$user = [
178+
'username' => 'Test User',
179+
'company_id' => '1',
180+
'country_id' => '1',
181+
'profile' => [
182+
'country' => 'United States of America and something more to violate the validation rule',
183+
],
184+
];
185+
186+
$userModel = model(UserModel::class);
187+
$userModel->with('profile')->useTransactions()->update(1, $user);
188+
189+
$this->assertArrayHasKey('country', $userModel->errors());
190+
$this->assertSame('The country field cannot exceed 20 characters in length.', $userModel->errors()['country']);
191+
192+
$this->dontSeeInDatabase(
193+
'users',
194+
[
195+
'id' => '1',
196+
'username' => 'Test User',
197+
]
198+
);
199+
}
200+
201+
public function testDatabaseErrorsOnInsert()
202+
{
203+
$user = [
204+
'username' => 'Test User 1',
205+
'company_id' => '1',
206+
'country_id' => '1',
207+
'profile' => [
208+
'country' => 'United States of America and something more to violate the validation rule',
209+
],
210+
];
211+
212+
$userModel = model(UserModel::class);
213+
$userModel->with('profile')->useTransactions()->insert($user);
214+
215+
$this->assertArrayHasKey('database_error', $userModel->errors());
216+
$this->assertSame(
217+
"Duplicate entry 'Test User 1' for key 'users.username'",
218+
$userModel->errors()['database_error']
219+
);
220+
221+
$this->dontSeeInDatabase(
222+
'users',
223+
[
224+
'id' => '3',
225+
'username' => 'Test User 1',
226+
]
227+
);
228+
}
229+
230+
public function testDatabaseErrorsOnUpdate()
231+
{
232+
$user = [
233+
'username' => 'Test User 3',
234+
'company_id' => '1',
235+
'country_id' => '11', // important
236+
'profile' => [
237+
'user_id' => '1',
238+
'country' => 'United States of America and something more to violate the validation rule',
239+
],
240+
];
241+
242+
$userModel = model(UserModel::class);
243+
$userModel->with('profile')->useTransactions()->update(1, $user);
244+
245+
$this->assertArrayHasKey('database_error', $userModel->errors());
246+
$this->assertStringContainsString(
247+
'Cannot add or update a child row: a foreign key constraint fails',
248+
$userModel->errors()['database_error']
249+
);
250+
251+
$this->dontSeeInDatabase(
252+
'users',
253+
[
254+
'id' => '1',
255+
'username' => 'Test User 3',
256+
]
257+
);
258+
}
148259
}

tests/_support/Database/Migrations/2024-11-28-073145_Relations.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ public function up(): void
135135
],
136136
]);
137137
$this->forge->addKey('id', true);
138+
$this->forge->addUniqueKey('username');
138139
$this->forge->addForeignKey('company_id', 'companies', 'id', 'CASCADE', 'CASCADE');
139140
$this->forge->addForeignKey('country_id', 'countries', 'id', 'CASCADE', 'CASCADE');
140141
$this->forge->createTable('users');

tests/_support/Models/ProfileModel.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ class ProfileModel extends Model
3333
protected $deletedField = 'deleted_at';
3434

3535
// Validation
36-
protected $validationRules = [];
36+
protected $validationRules = [
37+
'country' => 'max_length[20]',
38+
];
3739
protected $validationMessages = [];
3840
protected $skipValidation = false;
3941
protected $cleanValidationRules = true;

0 commit comments

Comments
 (0)