Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #[16222], Added Model::setRelated() #16402

Open
wants to merge 1 commit into
base: 5.0.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG-5.0.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Changelog

## [5.4.0](https://github.com/phalcon/cphalcon/releases/tag/v5.3.1) (xxxx-xx-xx)
## [5.4.0](https://github.com/phalcon/cphalcon/releases/tag/v5.4.0) (xxxx-xx-xx)

### Added
- 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)

### Fixed
- Model Annotation strategy did not work with empty_string [#16426] (https://github.com/phalcon/cphalcon/issues/16426)
Expand Down
191 changes: 104 additions & 87 deletions phalcon/Mvc/Model.zep
Original file line number Diff line number Diff line change
Expand Up @@ -412,97 +412,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 @@ -2115,6 +2033,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 <team@phalcon.io>
*
* 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);
}
}
Loading