diff --git a/CHANGELOG-5.0.md b/CHANGELOG-5.0.md index e3be7feee9..62bb581e09 100644 --- a/CHANGELOG-5.0.md +++ b/CHANGELOG-5.0.md @@ -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) diff --git a/phalcon/Mvc/Model.zep b/phalcon/Mvc/Model.zep index e5f5bf9b9a..659ef89e1c 100644 --- a/phalcon/Mvc/Model.zep +++ b/phalcon/Mvc/Model.zep @@ -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 = 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 = 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; } } @@ -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 = manager->getRelationByAlias( + className, + lowerAlias + ); + + if likely typeof relation === "object" { + let className = get_class(this), + manager = 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. * diff --git a/tests/database/Mvc/Model/SetRelatedCest.php b/tests/database/Mvc/Model/SetRelatedCest.php new file mode 100644 index 0000000000..7a8d8ff229 --- /dev/null +++ b/tests/database/Mvc/Model/SetRelatedCest.php @@ -0,0 +1,145 @@ + + * + * 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); + } +}