Skip to content

Commit

Permalink
Fix #[16222], Added Model::setRelated()
Browse files Browse the repository at this point in the history
  • Loading branch information
rudiservo committed Aug 15, 2023
1 parent 302c38d commit 6f0b665
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 87 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
### Added

- Added `Phalcon\Encryption\Security\JWT\Builder::addHeader()` to allow adding custom headers [#16396](https://github.com/phalcon/cphalcon/issues/16396)
- Added `Phalcon\Mvc\Model::setRelated()` to allow setting related models and automaticly de added to the dirtyRelated list [#16222] (https://github.com/phalcon/cphalcon/issues/16222)


## [5.2.3](https://github.com/phalcon/cphalcon/releases/tag/v5.2.3) (2023-07-26)
Expand Down
191 changes: 104 additions & 87 deletions phalcon/Mvc/Model.zep
Original file line number Diff line number Diff line change
Expand Up @@ -410,97 +410,15 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
*/
public function __set(string property, value)
{
var lowerProperty, modelName, manager, relation, referencedModel, item,
dirtyState;
array related;
var manager, related;

/**
* Values are probably relationships if they are objects
*/
if typeof value === "object" && value instanceof ModelInterface {
let lowerProperty = strtolower(property),
modelName = get_class(this),
manager = this->getModelsManager(),
relation = <RelationInterface> manager->getRelationByAlias(
modelName,
lowerProperty
);

if typeof relation === "object" {
let dirtyState = this->dirtyState;

if (value->getDirtyState() != dirtyState) {
let dirtyState = self::DIRTY_STATE_TRANSIENT;
}

unset this->related[lowerProperty];

let this->dirtyRelated[lowerProperty] = value,
this->dirtyState = dirtyState;

return value;
}
}

/**
* Check if the value is an array
*/
elseif typeof value === "array" {
let lowerProperty = strtolower(property),
modelName = get_class(this),
manager = this->getModelsManager(),
relation = <RelationInterface> manager->getRelationByAlias(
modelName,
lowerProperty
);

if typeof relation === "object" {
switch relation->getType() {
case Relation::BELONGS_TO:
case Relation::HAS_ONE:
/**
* Load referenced model from local cache if its possible
*/
let referencedModel = manager->load(
relation->getReferencedModel()
);

if typeof referencedModel === "object" {
referencedModel->assign(value);

unset this->related[lowerProperty];

let this->dirtyRelated[lowerProperty] = referencedModel,
this->dirtyState = self::DIRTY_STATE_TRANSIENT;

return value;
}

break;

case Relation::HAS_MANY:
case Relation::HAS_MANY_THROUGH:
let related = [];

for item in value {
if typeof item === "object" {
if item instanceof ModelInterface {
let related[] = item;
}
}
}

unset this->related[lowerProperty];

if count(related) > 0 {
let this->dirtyRelated[lowerProperty] = related,
this->dirtyState = self::DIRTY_STATE_TRANSIENT;
} else {
unset this->dirtyRelated[lowerProperty];
}

return value;
}
if (typeof value === "object" && value instanceof ModelInterface ) || typeof value === "array" {
let related = this->setRelated(property, value);
if null !== related {
return related;
}
}

Expand Down Expand Up @@ -2113,6 +2031,105 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
return result;
}

/**
* Sets related objects based on Alias and type of value (Model or array),
* by setting relations, the dirtyState are set acordingly to Transient has opt-in
*
* @param string alias
* @param mixed value
* @return \Phalcon\Mvc\Model|array|null Null is returned if no relation was found
*/
public function setRelated(string alias, value) -> mixed
{
var relation, className, manager, lowerAlias, referencedModel, item, related;

let manager = this->getModelsManager();
let className = get_class(this);
let lowerAlias = strtolower(alias);
/**
* Query the relation by alias
*/
let relation = <RelationInterface> manager->getRelationByAlias(
className,
lowerAlias
);

if likely typeof relation === "object" {
let className = get_class(this),
manager = <ManagerInterface> this->modelsManager,
lowerAlias = strtolower(alias);

if typeof value === "object" && value instanceof ModelInterface {
/**
* Opt-in dirty state
*/
value->setDirtyState(self::DIRTY_STATE_TRANSIENT);
let this->dirtyState = self::DIRTY_STATE_TRANSIENT;
/**
* Add to dirtyRelated and remove from related.
*/
let this->dirtyRelated[lowerAlias] = value;
unset(this->related[lowerAlias]);
return value;
}

/**
* Check if the value is an array
*/
elseif typeof value === "array" {
switch relation->getType() {
case Relation::BELONGS_TO:
case Relation::HAS_ONE:
/**
* Load referenced model from local cache if its possible
*/
let referencedModel = manager->load(
relation->getReferencedModel()
);

if typeof referencedModel === "object" {
referencedModel->assign(value);
let this->dirtyRelated[lowerAlias] = referencedModel;
/**
* Add to dirtyRelated and remove from related.
*/
unset(this->related[lowerAlias]);
return referencedModel;
}
break;

case Relation::HAS_MANY:
case Relation::HAS_MANY_THROUGH:
let related = [];
/**
* this is probably not needed
*/
for item in value {
if typeof item === "object" {
if item instanceof ModelInterface {
let related[] = item;
}
}
}
/**
* Add to dirtyRelated and remove from related.
*/
unset this->related[lowerAlias];

if count(related) > 0 {
let this->dirtyRelated[lowerAlias] = related,
this->dirtyState = self::DIRTY_STATE_TRANSIENT;
} else {
unset this->dirtyRelated[lowerAlias];
}

return value;
}
}
}
return null;
}

/**
* Checks if saved related records have already been loaded.
*
Expand Down
145 changes: 145 additions & 0 deletions tests/database/Mvc/Model/SetRelatedCest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

/**
* This file is part of the Phalcon Framework.
*
* (c) Phalcon Team <[email protected]>
*
* For the full copyright and license information, please view the LICENSE.txt
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\Tests\Database\Mvc\Model;

use DatabaseTester;
use PDO;
use Phalcon\Tests\Fixtures\Migrations\CustomersMigration;
use Phalcon\Tests\Fixtures\Migrations\InvoicesMigration;
use Phalcon\Tests\Fixtures\Traits\DiTrait;
use Phalcon\Tests\Models\Customers;
use Phalcon\Tests\Models\Invoices;

use function uniqid;

/**
* Class GetRelatedCest
*/
class SetRelatedCest
{
use DiTrait;

/**
* @param DatabaseTester $I
*/
public function _before(DatabaseTester $I)
{
$this->setNewFactoryDefault();
$this->setDatabase($I);
}

/**
* Tests Phalcon\Mvc\Model :: getRelated()
*
* @param DatabaseTester $I
*
* @since 2023-08-15
*
* @group mysql
* @group pgsql
* @group sqlite
*/
public function mvcModelSetRelated(DatabaseTester $I)
{
$I->wantToTest('Mvc\Model - setRelated()');

/** @var PDO $connection */
$connection = $I->getConnection();

$custId = 2;

$firstName = uniqid('cust-', true);
$lastName = uniqid('cust-', true);

$customersMigration = new CustomersMigration($connection);
$customersMigration->insert($custId, 0, $firstName, $lastName);

$paidInvoiceId = 4;
$unpaidInvoiceId = 5;

$title = uniqid('inv-');

$invoicesMigration = new InvoicesMigration($connection);
$invoicesMigration->insert(
$paidInvoiceId,
$custId,
Invoices::STATUS_PAID,
$title . '-paid'
);
$invoicesMigration->insert(
$unpaidInvoiceId,
$custId,
Invoices::STATUS_UNPAID,
$title . '-unpaid'
);

/**
* @var Customers $customer
*/
$customer = Customers::findFirst($custId);

$invoices = [];
$expectedTitle = [];
foreach ($customer->Invoices as $invoice) {
$invoices[] = $invoice;
$expectedTitle[] = $invoice->inv_title . 'updated';
$invoice->inv_title = $invoice->inv_title . 'updated';
}

$customer->setRelated('Invoices', $invoices);

$invoices = $customer->Invoices;

$I->assertIsArray($invoices);

$expected = 2;
$actual = count($invoices);
$I->assertEquals($expected, $actual);

$actual = $customer->save();

$I->assertTrue($actual);

$invoice = $invoices[0];
$actual = $invoice->getDirtyState();

$I->assertEquals(0, $actual);

$expected = $expectedTitle[0];
$actual = $invoice->inv_title;
$I->assertSame($expected, $actual);

$invoice = $invoices[1];
$actual = $invoice->getDirtyState();
$expected = 0;
$I->assertEquals($expected, $actual);

$expected = $expectedTitle[1];
$actual = $invoice->inv_title;
$I->assertSame($expected, $actual);

$actual = $customer->getDirtyState();
$expected = 0;
$I->assertEquals($expected, $actual);

$invoice->Customer = $customer;
$actual = $invoice->getDirtyState();
$expected = 1;
$I->assertEquals($expected, $actual);

$actual = $customer->getDirtyState();
$expected = 1;
$I->assertEquals($expected, $actual);
}
}

0 comments on commit 6f0b665

Please sign in to comment.