Skip to content

Commit d5a12bf

Browse files
authored
Merge pull request #16399 from rudiservo/i16395
Fixed infinite save loop #16395
2 parents fd62e2a + 9cfb819 commit d5a12bf

File tree

3 files changed

+74
-11
lines changed

3 files changed

+74
-11
lines changed

CHANGELOG-5.0.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [5.3.1](https://github.com/phalcon/cphalcon/releases/tag/v5.3.1) (xxxx-xx-xx)
4+
5+
### Fixed
6+
- Infinite save loop in Model::save() [#16395](https://github.com/phalcon/cphalcon/issues/16395)
7+
8+
39
## [5.3.0](https://github.com/phalcon/cphalcon/releases/tag/v5.3.0) (2023-08-15)
410

511
### Added

phalcon/Mvc/Model.zep

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ use Phalcon\Mvc\Model\TransactionInterface;
4141
use Phalcon\Mvc\Model\ValidationFailed;
4242
use Phalcon\Mvc\ModelInterface;
4343
use Phalcon\Filter\Validation\ValidationInterface;
44+
use Phalcon\Support\Collection;
45+
use Phalcon\Support\Collection\CollectionInterface;
4446
use Serializable;
4547

4648
/**
@@ -2579,12 +2581,35 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
25792581
* $robot->save();
25802582
*```
25812583
*/
2584+
25822585
public function save() -> bool
2586+
{
2587+
var visited;
2588+
let visited = new Collection();
2589+
return this->doSave(visited);
2590+
}
2591+
2592+
/**
2593+
* Inserted or updates model instance, expects a visited list of objects.
2594+
*
2595+
* @param CollectionInterface $visited
2596+
*
2597+
* @return bool
2598+
*/
2599+
public function doSave(<CollectionInterface> visited) -> bool
25832600
{
25842601
var metaData, schema, writeConnection, readConnection, source, table,
2585-
identityField, exists, success, relatedToSave;
2602+
identityField, exists, success, relatedToSave, objId;
25862603
bool hasRelatedToSave;
25872604

2605+
let objId = spl_object_id(this);
2606+
2607+
if true === visited->has(objId) {
2608+
return true;
2609+
}
2610+
2611+
visited->set(objId, this);
2612+
25882613
let metaData = this->getModelsMetaData();
25892614

25902615
/**
@@ -2610,7 +2635,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
26102635
let hasRelatedToSave = count(relatedToSave) > 0;
26112636

26122637
if hasRelatedToSave {
2613-
if this->preSaveRelatedRecords(writeConnection, relatedToSave) === false {
2638+
if this->preSaveRelatedRecords(writeConnection, relatedToSave, visited) === false {
26142639
return false;
26152640
}
26162641
}
@@ -2695,7 +2720,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
26952720
/**
26962721
* Change the dirty state to persistent
26972722
*/
2698-
if success {
2723+
if true === success {
26992724
let this->dirtyState = self::DIRTY_STATE_PERSISTENT;
27002725
}
27012726

@@ -2711,7 +2736,8 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
27112736
*/
27122737
let success = this->postSaveRelatedRecords(
27132738
writeConnection,
2714-
relatedToSave
2739+
relatedToSave,
2740+
visited
27152741
);
27162742
}
27172743
}
@@ -4964,9 +4990,11 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
49644990
* Saves related records that must be stored prior to save the master record
49654991
*
49664992
* @param ModelInterface[] related
4993+
* @param CollectionInterface visited
49674994
* @return bool
49684995
*/
4969-
protected function preSaveRelatedRecords(<AdapterInterface> connection, related) -> bool
4996+
4997+
protected function preSaveRelatedRecords(<AdapterInterface> connection, related, <CollectionInterface> visited) -> bool
49704998
{
49714999
var className, manager, type, relation, columns, referencedFields, nesting, name, record;
49725000

@@ -5006,7 +5034,6 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
50065034
"Only objects can be stored as part of belongs-to relations in '" . get_class(this) . "' Relation " . name
50075035
);
50085036
}
5009-
50105037
let columns = relation->getFields(),
50115038
referencedFields = relation->getReferencedFields();
50125039
// let columns = relation->getFields(),
@@ -5023,7 +5050,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
50235050
* If dynamic update is enabled, saving the record must not take any action
50245051
* Only save if the model is dirty to prevent circular relations causing an infinite loop
50255052
*/
5026-
if record->dirtyState !== Model::DIRTY_STATE_PERSISTENT && !record->save() {
5053+
if record->dirtyState !== Model::DIRTY_STATE_PERSISTENT && !record->doSave(visited) {
50275054
/**
50285055
* Get the validation messages generated by the
50295056
* referenced model
@@ -5072,9 +5099,10 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
50725099
* Save the related records assigned in the has-one/has-many relations
50735100
*
50745101
* @param ModelInterface[] related
5102+
* @param CollectionInterface visited
50755103
* @return bool
50765104
*/
5077-
protected function postSaveRelatedRecords(<AdapterInterface> connection, related) -> bool
5105+
protected function postSaveRelatedRecords(<AdapterInterface> connection, related, <CollectionInterface> visited) -> bool
50785106
{
50795107
var nesting, className, manager, relation, name, record,
50805108
columns, referencedModel, referencedFields, relatedRecords, value,
@@ -5157,7 +5185,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
51575185
/**
51585186
* Save the record and get messages
51595187
*/
5160-
if !recordAfter->save() {
5188+
if !recordAfter->doSave(visited) {
51615189
/**
51625190
* Get the validation messages generated by the
51635191
* referenced model
@@ -5221,7 +5249,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
52215249
/**
52225250
* Save the record and get messages
52235251
*/
5224-
if !intermediateModel->save() {
5252+
if !intermediateModel->doSave(visited) {
52255253
/**
52265254
* Get the validation messages generated by the referenced model
52275255
*/
@@ -5244,7 +5272,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
52445272
/**
52455273
* Save the record and get messages
52465274
*/
5247-
if !recordAfter->save() {
5275+
if !recordAfter->doSave(visited) {
52485276
/**
52495277
* Get the validation messages generated by the
52505278
* referenced model

tests/database/Mvc/Model/SaveCest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,35 @@ public function mvcModelSaveWithRelatedManyAndBelongsRecordsProperty(DatabaseTes
644644
$I->assertEquals($expected, $actual);
645645
}
646646

647+
/**
648+
* Tests Phalcon\Mvc\Model\ :: save() Infinite Loop
649+
*
650+
* @author Phalcon Team <[email protected]>
651+
* @since 2023-08-09
652+
* @issue https://github.com/phalcon/cphalcon/issues/16395
653+
*
654+
* @group mysql
655+
* @group sqlite
656+
*/
657+
public function infiniteSaveLoop(DatabaseTester $I)
658+
{
659+
$I->wantToTest('Mvc\Model - save() infinite Save loop');
660+
661+
/** @var \PDO $connection */
662+
$connection = $I->getConnection();
663+
$invoicesMigration = new InvoicesMigration($connection);
664+
$invoicesMigration->insert(77, 1, 0, uniqid('inv-', true));
665+
666+
$customersMigration = new CustomersMigration($connection);
667+
$customersMigration->insert(1, 1, 'test_firstName_1', 'test_lastName_1');
668+
669+
$customer = Customers::findFirst(1);
670+
$invoice = Invoices::findFirst(77);
671+
$invoice->customer = $customer;
672+
$customer->invoices = [$invoice];
673+
$customer->save();
674+
}
675+
647676
/**
648677
* @return \string[][]
649678
*/

0 commit comments

Comments
 (0)