Skip to content

Commit

Permalink
Fixed infinite save loop phalcon#16395
Browse files Browse the repository at this point in the history
  • Loading branch information
rudiservo committed Aug 10, 2023
1 parent f3f6cc8 commit 3b128a1
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 10 deletions.
45 changes: 35 additions & 10 deletions phalcon/Mvc/Model.zep
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use Phalcon\Mvc\Model\ValidationFailed;
use Phalcon\Mvc\ModelInterface;
use Phalcon\Filter\Validation\ValidationInterface;
use Serializable;
use SplObjectStorage;

/**
* Phalcon\Mvc\Model
Expand Down Expand Up @@ -2579,12 +2580,34 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
* $robot->save();
*```
*/

public function save() -> bool
{
var visited;
let visited = new SplObjectStorage();
return this->doSave(visited);
}

/**
* Inserted or updates model instance, expects a visited list of objects.
*
* @param SplObjectStorage $visited
*
* @return bool
*/

public function doSave(<SplObjectStorage> visited) -> bool
{
var metaData, schema, writeConnection, readConnection, source, table,
identityField, exists, success, relatedToSave;
bool hasRelatedToSave;

if true === visited->contains(this) {
return true;
}

visited->attach(this);

let metaData = this->getModelsMetaData();

/**
Expand All @@ -2610,7 +2633,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
let hasRelatedToSave = count(relatedToSave) > 0;

if hasRelatedToSave {
if this->preSaveRelatedRecords(writeConnection, relatedToSave) === false {
if this->preSaveRelatedRecords(writeConnection, relatedToSave, visited) === false {
return false;
}
}
Expand Down Expand Up @@ -2695,7 +2718,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
/**
* Change the dirty state to persistent
*/
if success {
if true === success{
let this->dirtyState = self::DIRTY_STATE_PERSISTENT;
}

Expand All @@ -2711,7 +2734,8 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
*/
let success = this->postSaveRelatedRecords(
writeConnection,
relatedToSave
relatedToSave,
visited
);
}
}
Expand Down Expand Up @@ -4964,9 +4988,10 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
* Saves related records that must be stored prior to save the master record
*
* @param ModelInterface[] related
* @param SplObjectStorage visited
* @return bool
*/
protected function preSaveRelatedRecords(<AdapterInterface> connection, related) -> bool
protected function preSaveRelatedRecords(<AdapterInterface> connection, related, <SplObjectStorage> visited) -> bool
{
var className, manager, type, relation, columns, referencedFields, nesting, name, record;

Expand All @@ -4981,6 +5006,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
manager = <ManagerInterface> this->getModelsManager();

for name, record in related {

/**
* Try to get a relation with the same name
*/
Expand All @@ -5006,7 +5032,6 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
"Only objects can be stored as part of belongs-to relations in '" . get_class(this) . "' Relation " . name
);
}

let columns = relation->getFields(),
referencedFields = relation->getReferencedFields();
// let columns = relation->getFields(),
Expand All @@ -5023,7 +5048,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
* If dynamic update is enabled, saving the record must not take any action
* Only save if the model is dirty to prevent circular relations causing an infinite loop
*/
if record->dirtyState !== Model::DIRTY_STATE_PERSISTENT && !record->save() {
if record->dirtyState !== Model::DIRTY_STATE_PERSISTENT && !record->doSave(visited) {
/**
* Get the validation messages generated by the
* referenced model
Expand Down Expand Up @@ -5074,7 +5099,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
* @param ModelInterface[] related
* @return bool
*/
protected function postSaveRelatedRecords(<AdapterInterface> connection, related) -> bool
protected function postSaveRelatedRecords(<AdapterInterface> connection, related, <SplObjectStorage> visited) -> bool
{
var nesting, className, manager, relation, name, record,
columns, referencedModel, referencedFields, relatedRecords, value,
Expand Down Expand Up @@ -5157,7 +5182,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
/**
* Save the record and get messages
*/
if !recordAfter->save() {
if !recordAfter->doSave(visited) {
/**
* Get the validation messages generated by the
* referenced model
Expand Down Expand Up @@ -5221,7 +5246,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
/**
* Save the record and get messages
*/
if !intermediateModel->save() {
if !intermediateModel->doSave(visited) {
/**
* Get the validation messages generated by the referenced model
*/
Expand All @@ -5244,7 +5269,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
/**
* Save the record and get messages
*/
if !recordAfter->save() {
if !recordAfter->doSave(visited) {
/**
* Get the validation messages generated by the
* referenced model
Expand Down
29 changes: 29 additions & 0 deletions tests/database/Mvc/Model/SaveCest.php
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,35 @@ public function mvcModelSaveWithRelatedManyAndBelongsRecordsProperty(DatabaseTes
$I->assertEquals($expected, $actual);
}

/**
* Tests Phalcon\Mvc\Model\ :: save() Infinite Loop
*
* @author Phalcon Team <team@phalcon.io>
* @since 2023-08-09
* @issue #16395
*
* @group mysql
* @group sqlite
*/
public function infiniteSaveLoop(DatabaseTester $I)
{
$I->wantToTest('Mvc\Model - save() infinite Save loop');

/** @var \PDO $connection */
$connection = $I->getConnection();
$invoicesMigration = new InvoicesMigration($connection);
$invoicesMigration->insert(77, 1, 0, uniqid('inv-', true));

$customersMigration = new CustomersMigration($connection);
$customersMigration->insert(1, 1, 'test_firstName_1', 'test_lastName_1');

$customer = Customers::findFirst(1);
$invoice = Invoices::findFirst(77);
$invoice->customer = $customer;
$customer->invoices = [ $invoice ];
$customer->save();
}

/**
* @return \string[][]
*/
Expand Down

0 comments on commit 3b128a1

Please sign in to comment.