From 736c0490b1378d9de43ae0b404e888af4fddc540 Mon Sep 17 00:00:00 2001 From: Rudi Servo Date: Wed, 16 Aug 2023 14:03:04 +0000 Subject: [PATCH] Fix #16029, added ability to save Multiple fields --- CHANGELOG-5.0.md | 7 +- phalcon/Mvc/Model.zep | 192 +++--- phalcon/Mvc/Model/Manager.zep | 59 +- tests/_data/assets/schemas/mysql.sql | 10 +- tests/_data/assets/schemas/pgsql.sql | 15 +- tests/_data/assets/schemas/sqlite.sql | 30 + .../fixtures/Migrations/OrdersMigration.php | 21 +- .../Migrations/OrdersProductsMigration.php | 42 +- .../fixtures/Migrations/ProductsMigration.php | 21 +- tests/_data/fixtures/models/Customers.php | 13 + tests/_data/fixtures/models/Invoices.php | 10 + tests/_data/fixtures/models/Orders.php | 2 + .../_data/fixtures/models/OrdersMultiple.php | 55 ++ .../_data/fixtures/models/OrdersProducts.php | 6 + tests/_data/fixtures/models/Products.php | 2 + tests/database/Mvc/Model/RelationsCest.php | 561 ++++++++++++++++++ 16 files changed, 912 insertions(+), 134 deletions(-) create mode 100644 tests/_data/fixtures/models/OrdersMultiple.php create mode 100644 tests/database/Mvc/Model/RelationsCest.php diff --git a/CHANGELOG-5.0.md b/CHANGELOG-5.0.md index e9723040188..1900b44b394 100644 --- a/CHANGELOG-5.0.md +++ b/CHANGELOG-5.0.md @@ -1,8 +1,13 @@ # Changelog -## [5.3.1](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 + - Infinite save loop in Model::save() [#16395](https://github.com/phalcon/cphalcon/issues/16395) diff --git a/phalcon/Mvc/Model.zep b/phalcon/Mvc/Model.zep index e5f5bf9b9ac..515acb20d95 100644 --- a/phalcon/Mvc/Model.zep +++ b/phalcon/Mvc/Model.zep @@ -4996,8 +4996,8 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, protected function preSaveRelatedRecords( connection, related, visited) -> bool { - var className, manager, type, relation, columns, referencedFields, nesting, name, record; - + var className, manager, type, relation, columns, referencedFields, nesting, name, record, columnA, columnB; + int columnCount, i; let nesting = false; /** @@ -5034,17 +5034,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(), -// referencedModel = relation->getReferencedModel(), -// referencedFields = relation->getReferencedFields(); - - if unlikely typeof columns === "array" { - connection->rollback(nesting); - - throw new Exception("Not implemented in '" . get_class(this) . "' Relation " . name); - } /** * If dynamic update is enabled, saving the record must not take any action @@ -5069,7 +5058,18 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, * Read the attribute from the referenced model and assign * it to the current model */ - let this->{columns} = record->readAttribute(referencedFields); + let columns = relation->getFields(), + referencedFields = relation->getReferencedFields(); + if unlikely typeof columns === "array" { + let columnCount = count(columns) - 1; + for i in range(0, columnCount) { + let columnA = columns[i]; + let columnB = referencedFields[i]; + let this->{columnA} = record->{columnB}; + } + } else { + let this->{columns} = record->{referencedFields}; + } } } } @@ -5105,11 +5105,14 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, protected function postSaveRelatedRecords( connection, related, visited) -> bool { var nesting, className, manager, relation, name, record, - columns, referencedModel, referencedFields, relatedRecords, value, + columns, referencedModel, referencedFields, relatedRecords, recordAfter, intermediateModel, intermediateFields, - intermediateValue, intermediateModelName, - intermediateReferencedFields, existingIntermediateModel; + intermediateModelName, + intermediateReferencedFields, existingIntermediateModel, columnA, columnB; bool isThrough; + int columnCount, referencedFieldsCount, i; + string intermediateConditions; + array conditions, placeholders; let nesting = false, className = get_class(this), @@ -5144,12 +5147,6 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, referencedModel = relation->getReferencedModel(), referencedFields = relation->getReferencedFields(); - if unlikely typeof columns === "array" { - connection->rollback(nesting); - - throw new Exception("Not implemented in '" . className . "' on Relation " . name); - } - /** * Create an implicit array for has-many/has-one records */ @@ -5159,18 +5156,6 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, let relatedRecords = record; } - if unlikely !fetch value, this->{columns} { - connection->rollback(nesting); - - throw new Exception( - "The column '" . columns . "' needs to be present in the model '" . className . "'" - ); - } - - /** - * Get the value of the field from the current model - * Check if the relation is a has-many-to-many - */ let isThrough = (bool) relation->isThrough(); /** @@ -5180,7 +5165,30 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, let intermediateModelName = relation->getIntermediateModel(), intermediateFields = relation->getIntermediateFields(), intermediateReferencedFields = relation->getIntermediateReferencedFields(); - + if unlikely typeof columns === "array" { + let columnCount = count(columns) - 1; + if relation->getType() == Relation::HAS_ONE_THROUGH { + let placeholders = []; + let conditions = []; + for i in range(0, columnCount) { + let columnA = columns[i]; + let conditions[] = "[". intermediateFields[i] . "] = :APR" . i . ":", + placeholders["APR" . i] = this->{columnA}; + } + let intermediateConditions = join(" AND ", conditions); + } + } else { + if relation->getType() == Relation::HAS_ONE_THROUGH { + let placeholders = []; + let intermediateConditions = "[" . intermediateFields . "] = ?0"; + let placeholders[] = this->{columns}; + } + } + if unlikely typeof referencedFields === "array" { + let referencedFieldsCount = count(referencedFields) - 1; + } else { + let referencedFieldsCount = null; + } for recordAfter in relatedRecords { /** * Save the record and get messages @@ -5213,8 +5221,8 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, if relation->getType() == Relation::HAS_ONE_THROUGH { let existingIntermediateModel = intermediateModel->findFirst( [ - "[" . intermediateFields . "] = ?0", - "bind": [value] + intermediateConditions, + "bind": placeholders ] ); @@ -5222,29 +5230,30 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, let intermediateModel = existingIntermediateModel; } } - - /** - * Write value in the intermediate model - */ - intermediateModel->writeAttribute( - intermediateFields, - value - ); - - /** - * Get the value from the referenced model - */ - let intermediateValue = recordAfter->readAttribute( - referencedFields - ); - - /** - * Write the intermediate value in the intermediate model - */ - intermediateModel->writeAttribute( - intermediateReferencedFields, - intermediateValue - ); + if unlikely typeof columns === "array" { + for i in range(0, columnCount) { + let columnA = columns[i]; + let columnB = intermediateFields[i]; + let intermediateModel->{columnB} = this->{columnA}; + } + } else { + /** + * Write value in the intermediate model + */ + let intermediateModel->{intermediateFields} = this->{columns}; + } + if unlikely typeof referencedFields === "array" { + for i in range(0, referencedFieldsCount) { + let columnA = referencedFields[i]; + let columnB = intermediateReferencedFields[i]; + let intermediateModel->{columnB} = recordAfter->{columnA}; + } + } else { + /** + * Write the intermediate value in the intermediate model + */ + let intermediateModel->{intermediateReferencedFields} = recordAfter->{referencedFields}; + } /** * Save the record and get messages @@ -5264,27 +5273,56 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, } } } else { - for recordAfter in relatedRecords { - /** - * Assign the value to the - */ - recordAfter->writeAttribute(referencedFields, value); - /** - * Save the record and get messages - */ - if !recordAfter->doSave(visited) { + if unlikely typeof columns === "array" { + let columnCount = count(columns) - 1; + for recordAfter in relatedRecords { + for i in range(0, columnCount) { + let columnA = columns[i]; + let columnB = referencedFields[i]; + let recordAfter->{columnB} = this->{columnA}; + } /** - * Get the validation messages generated by the - * referenced model + * Save the record and get messages */ - this->appendMessagesFrom(recordAfter); - + if !recordAfter->doSave(visited) { + /** + * Get the validation messages generated by the + * referenced model + */ + this->appendMessagesFrom(recordAfter); + + /** + * Rollback the implicit transaction + */ + connection->rollback(nesting); + + return false; + } + } + } else { + for recordAfter in relatedRecords { /** - * Rollback the implicit transaction + * Assign the value to the */ - connection->rollback(nesting); + let recordAfter->{referencedFields} = this->{columns}; + /** + * Save the record and get messages + */ + if !recordAfter->doSave(visited) { - return false; + /** + * Get the validation messages generated by the + * referenced model + */ + this->appendMessagesFrom(recordAfter); + + /** + * Rollback the implicit transaction + */ + connection->rollback(nesting); + + return false; + } } } } diff --git a/phalcon/Mvc/Model/Manager.zep b/phalcon/Mvc/Model/Manager.zep index 99cee971aef..2e1b740b2a2 100644 --- a/phalcon/Mvc/Model/Manager.zep +++ b/phalcon/Mvc/Model/Manager.zep @@ -289,10 +289,10 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI /** * Check if the number of fields are the same */ - if typeof referencedFields == "array" { + if unlikely typeof referencedFields == "array" { if unlikely count(fields) != count(referencedFields) { throw new Exception( - "Number of referenced fields are not the same" + "Number of referenced fields are not the same in the BelongsTo relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'" ); } } @@ -313,7 +313,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI */ if fetch alias, options["alias"] { if unlikely typeof alias != "string" { - throw new Exception("Relation alias must be a string"); + throw new Exception("Relation alias must be a string in the BelongsTo relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'"); } let lowerAlias = strtolower(alias); @@ -388,7 +388,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI if typeof referencedFields == "array" { if unlikely count(fields) != count(referencedFields) { throw new Exception( - "Number of referenced fields are not the same" + "Number of referenced fields are not the same in the HasMany relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'" ); } } @@ -409,7 +409,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI */ if fetch alias, options["alias"] { if unlikely typeof alias != "string" { - throw new Exception("Relation alias must be a string"); + throw new Exception("Relation alias must be a string in the HasMany relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'"); } let lowerAlias = strtolower(alias); @@ -492,7 +492,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI if typeof intermediateFields == "array" { if unlikely count(fields) != count(intermediateFields) { throw new Exception( - "Number of referenced fields are not the same" + "Number of referenced fields are not the same in the HasManytoMany relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'" ); } } @@ -504,7 +504,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI if typeof intermediateReferencedFields == "array" { if unlikely count(fields) != count(intermediateFields) { throw new Exception( - "Number of referenced fields are not the same" + "Number of referenced fields are not the same in the HasManytoMany relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'" ); } } @@ -534,7 +534,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI */ if fetch alias, options["alias"] { if typeof alias != "string" { - throw new Exception("Relation alias must be a string"); + throw new Exception("Relation alias must be a string in the HasManytoMany relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'"); } let lowerAlias = strtolower(alias); @@ -614,7 +614,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI if typeof referencedFields == "array" { if unlikely count(fields) != count(referencedFields) { throw new Exception( - "Number of referenced fields are not the same" + "Number of referenced fields are not the same in the HasOne relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'" ); } } @@ -635,7 +635,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI */ if fetch alias, options["alias"] { if unlikely typeof alias != "string" { - throw new Exception("Relation alias must be a string"); + throw new Exception("Relation alias must be a string in the HasOne relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'"); } let lowerAlias = strtolower(alias); @@ -718,7 +718,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI if typeof intermediateFields == "array" { if unlikely count(fields) != count(intermediateFields) { throw new Exception( - "Number of referenced fields are not the same" + "Number of referenced fields are not the same in the HasOneThrough relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'" ); } } @@ -730,7 +730,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI if typeof intermediateReferencedFields == "array" { if unlikely count(fields) != count(intermediateFields) { throw new Exception( - "Number of referenced fields are not the same" + "Number of referenced fields are not the same in the HasOneThrough relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'" ); } } @@ -760,7 +760,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI */ if fetch alias, options["alias"] { if typeof alias != "string" { - throw new Exception("Relation alias must be a string"); + throw new Exception("Relation alias must be a string in the HasOneThrough relation of model '" . entityName . "' with Reference Model'" . referencedEntity . "'"); } let lowerAlias = strtolower(alias); @@ -1341,13 +1341,14 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI var parameters = null, string method = null ) { - var referencedModel, intermediateModel, intermediateFields, fields, + var referencedModel, intermediateModel, intermediateFields, intermediateReferenceFields, fields, builder, extraParameters, refPosition, field, referencedFields, findParams, findArguments, uniqueKey, records, arguments, rows, firstRow, query; array placeholders, conditions, joinConditions; bool reusable; string retrieveMethod; + int i, columnCount; /** * Re-use bound parameters @@ -1379,27 +1380,33 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI * relation */ let fields = relation->getFields(); - - if unlikely typeof fields == "array" { - throw new Exception("Not supported"); - } - - let conditions[] = "[" . intermediateModel . "].[" . intermediateFields . "] = :APR0:", + if unlikely typeof fields === "array" { + let columnCount = count(fields) - 1; + for i in range(0, columnCount) { + let conditions[] = "[" . intermediateModel . "].[". intermediateFields[i] . "] = :APR" . i . ":", + placeholders["APR" . i] = record->readAttribute(fields[i]); + } + } else { + let conditions[] = "[" . intermediateModel . "].[" . intermediateFields . "] = :APR0:", placeholders["APR0"] = record->readAttribute(fields); + } let joinConditions = []; /** * Create the join conditions */ - let intermediateFields = relation->getIntermediateReferencedFields(); - - if unlikely typeof intermediateFields == "array" { - throw new Exception("Not supported"); + let intermediateReferenceFields = relation->getIntermediateReferencedFields(); + let referencedFields = relation->getReferencedFields(); + if unlikely typeof intermediateReferenceFields === "array" { + let columnCount = count(intermediateReferenceFields) - 1; + for i in range(0, columnCount) { + let joinConditions[] = "[" . intermediateModel . "].[" . intermediateReferenceFields[i] . "] = [" . referencedModel . "].[" . referencedFields[i] . "]"; + } + } else { + let joinConditions[] = "[" . intermediateModel . "].[" . intermediateReferenceFields . "] = [" . referencedModel . "].[" . referencedFields . "]"; } - let joinConditions[] = "[" . intermediateModel . "].[" . intermediateFields . "] = [" . referencedModel . "].[" . relation->getReferencedFields() . "]"; - /** * We don't trust the user or the database so we use bound parameters * Create a query builder diff --git a/tests/_data/assets/schemas/mysql.sql b/tests/_data/assets/schemas/mysql.sql index dbd9f6dbf31..4f7c551349f 100644 --- a/tests/_data/assets/schemas/mysql.sql +++ b/tests/_data/assets/schemas/mysql.sql @@ -189,17 +189,20 @@ drop table if exists `co_orders`; CREATE TABLE `co_orders` ( `ord_id` int(10) unsigned NOT NULL AUTO_INCREMENT, `ord_name` VARCHAR(70) NULL, + `ord_status_flag` tinyint(1) NULL, PRIMARY KEY (`ord_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -drop table if exists private.`co_orders_x_products`; +drop table if exists `private`.`co_orders_x_products`; -CREATE TABLE private.`co_orders_x_products` ( +CREATE TABLE `private`.`co_orders_x_products` ( `oxp_ord_id` int(10) unsigned NOT NULL, + `oxp_ord_status_flag` tinyint(1) NULL, `oxp_prd_id` int(10) unsigned NOT NULL, - `oxp_quantity` int(10) unsigned NOT NULL, + `oxp_prd_status_flag` tinyint(1) NULL, + `oxp_quantity` int(10) unsigned NULL, PRIMARY KEY (`oxp_ord_id`, `oxp_prd_id` ) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -235,6 +238,7 @@ drop table if exists `co_products`; CREATE TABLE `co_products` ( `prd_id` int(10) unsigned NOT NULL AUTO_INCREMENT, `prd_name` VARCHAR(70) NULL, + `prd_status_flag` tinyint(1) NULL, PRIMARY KEY (`prd_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/tests/_data/assets/schemas/pgsql.sql b/tests/_data/assets/schemas/pgsql.sql index 8f7afc24892..3d298e8481a 100644 --- a/tests/_data/assets/schemas/pgsql.sql +++ b/tests/_data/assets/schemas/pgsql.sql @@ -116,18 +116,22 @@ create table co_orders ord_id serial not null constraint ord_pk primary key, - ord_name varchar(70) + ord_name varchar(70), + ord_status_flag integer ); -drop table if exists private.co_orders_x_products; +drop table if exists co_orders_x_products; -create table private.co_orders_x_products +create table co_orders_x_products ( oxp_ord_id int not null, oxp_prd_id int not null, - oxp_quantity int not null + oxp_quantity int null, + oxp_ord_status_flag integer, + oxp_prd_status_flag integer + ); @@ -141,7 +145,8 @@ create table co_products prd_id serial not null constraint prd_pk primary key, - prd_name varchar(70) + prd_name varchar(70), + prd_status_flag integer ); diff --git a/tests/_data/assets/schemas/sqlite.sql b/tests/_data/assets/schemas/sqlite.sql index d7a81989223..dc2e08ff397 100644 --- a/tests/_data/assets/schemas/sqlite.sql +++ b/tests/_data/assets/schemas/sqlite.sql @@ -139,3 +139,33 @@ create table stuff stf_type integer not null ); + +drop table if exists `co_orders`; + +create table `co_orders` ( + `ord_id` integer constraint ord_id_pk primary key autoincrement, + `ord_name` text NULL, + `ord_status_flag` integer NULL + ); + + +drop table if exists `co_products`; + +create table `co_products` ( + `prd_id` integer constraint prd_id_pk primary key autoincrement, + `prd_name` text NULL, + `prd_status_flag` integer NULL + ); + + + + +drop table if exists co_orders_x_products; + +create table co_orders_x_products ( + `oxp_ord_id` integer NOT NULL, + `oxp_ord_status_flag` integer NULL, + `oxp_prd_id` integer NOT NULL, + `oxp_prd_status_flag` integer NULL, + `oxp_quantity` integer NULL +); diff --git a/tests/_data/fixtures/Migrations/OrdersMigration.php b/tests/_data/fixtures/Migrations/OrdersMigration.php index b9dbe6a7a38..e6a2dcb49f9 100644 --- a/tests/_data/fixtures/Migrations/OrdersMigration.php +++ b/tests/_data/fixtures/Migrations/OrdersMigration.php @@ -28,15 +28,17 @@ class OrdersMigration extends AbstractMigration */ public function insert( $ord_id, - string $ord_name = null + string $ord_name = null, + int $ord_status_flag = 0 ): int { $ord_id = $ord_id ?: 'null'; $ord_name = $ord_name ?: uniqid(); + $ord_status_flag = $ord_status_flag ?: 0; $sql = <<hasMany( + ['cst_id', 'cst_status_flag'], + Invoices::class, + ['inv_cst_id', 'inv_status_flag'], + [ + 'alias' => 'invoicesMultipleFields', + 'reusable' => true, + 'foreignKey' => [ + 'action' => Model\Relation::NO_ACTION + ] + ] + ); } /** diff --git a/tests/_data/fixtures/models/Invoices.php b/tests/_data/fixtures/models/Invoices.php index 04eea939899..a535cd73f8b 100644 --- a/tests/_data/fixtures/models/Invoices.php +++ b/tests/_data/fixtures/models/Invoices.php @@ -60,6 +60,16 @@ public function initialize() 'reusable' => true, ] ); + + $this->belongsTo( + ['inv_cst_id', 'inv_status_flag'], + Customers::class, + ['cst_id', 'cst_status_flag'], + [ + 'alias' => 'customerMultipleFields', + 'reusable' => true, + ] + ); } /** diff --git a/tests/_data/fixtures/models/Orders.php b/tests/_data/fixtures/models/Orders.php index 565236593cd..63f6418d395 100644 --- a/tests/_data/fixtures/models/Orders.php +++ b/tests/_data/fixtures/models/Orders.php @@ -20,11 +20,13 @@ * * @property int $ord_id; * @property string $ord_name; + * @property int $ord_status_flag; */ class Orders extends Model { public $ord_id; public $ord_name; + public $ord_status_flag; public function initialize() { diff --git a/tests/_data/fixtures/models/OrdersMultiple.php b/tests/_data/fixtures/models/OrdersMultiple.php new file mode 100644 index 00000000000..20f7aed0169 --- /dev/null +++ b/tests/_data/fixtures/models/OrdersMultiple.php @@ -0,0 +1,55 @@ + + * + * 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\Models; + +use Phalcon\Mvc\Model; + +/** + * Class Orders + * + * @property int $ord_id; + * @property string $ord_name; + * @property int $ord_status_flag; + */ +class OrdersMultiple extends Orders +{ + public function initialize() + { + parent::initialize(); + + $this->hasManyToMany( + ['ord_id', 'ord_status_flag'], + OrdersProducts::class, + ['oxp_ord_id', 'oxp_ord_status_flag'], + ['oxp_prd_id', 'oxp_prd_status_flag'], + Products::class, + ['prd_id', 'prd_status_flag'], + [ + 'alias' => 'productsMultipleFields' + ] + ); + + $this->hasOneThrough( + ['ord_id', 'ord_status_flag'], + OrdersProducts::class, + ['oxp_ord_id', 'oxp_ord_status_flag'], + ['oxp_prd_id', 'oxp_prd_status_flag'], + Products::class, + ['prd_id', 'prd_status_flag'], + [ + 'alias' => 'singleProductMultipleFields' + ] + ); + } +} diff --git a/tests/_data/fixtures/models/OrdersProducts.php b/tests/_data/fixtures/models/OrdersProducts.php index 6dacddf71f8..6972d6559b9 100644 --- a/tests/_data/fixtures/models/OrdersProducts.php +++ b/tests/_data/fixtures/models/OrdersProducts.php @@ -21,11 +21,17 @@ * @property int $oxp_ord_id; * @property string $oxp_prd_id; * @property string $oxp_quantity; + * @property int $oxp_ord_status_flag; + * @property int $oxp_prd_status_flag; */ class OrdersProducts extends Model { public $oxp_ord_id; public $oxp_prd_id; + public $oxp_quantity; + public $oxp_ord_status_flag; + public $oxp_prd_status_flag; + public function initialize() { diff --git a/tests/_data/fixtures/models/Products.php b/tests/_data/fixtures/models/Products.php index 25531c1f0b1..33b416c2b6c 100644 --- a/tests/_data/fixtures/models/Products.php +++ b/tests/_data/fixtures/models/Products.php @@ -20,11 +20,13 @@ * * @property int $prd_id; * @property string $prd_name; + * @property int $prd_status_flag; */ class Products extends Model { public $prd_id; public $prd_name; + public $prd_status_flag; public function initialize() { diff --git a/tests/database/Mvc/Model/RelationsCest.php b/tests/database/Mvc/Model/RelationsCest.php new file mode 100644 index 00000000000..e4b4d86e830 --- /dev/null +++ b/tests/database/Mvc/Model/RelationsCest.php @@ -0,0 +1,561 @@ + + * + * 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\Migrations\OrdersMigration; +use Phalcon\Tests\Fixtures\Migrations\OrdersProductsMigration; +use Phalcon\Tests\Fixtures\Migrations\ProductsMigration; +use Phalcon\Tests\Fixtures\Traits\DiTrait; +use Phalcon\Tests\Models\Customers; +use Phalcon\Tests\Models\Invoices; +use Phalcon\Tests\Models\OrdersMultiple; +use Phalcon\Tests\Models\OrdersProducts; +use Phalcon\Tests\Models\Products; + +use function uniqid; + +/** + * Class GetRelatedCest + */ +class RelationsCest +{ + use DiTrait; + + /** + * @param DatabaseTester $I + */ + public function _before(DatabaseTester $I) + { + $this->setNewFactoryDefault(); + $this->setDatabase($I); + } + + /** + * Tests Phalcon\Mvc\Model :: BelongsTo() - get + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + * @group sqlite + */ + public function mvcModelGetBelongsTo(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - get - BelongsTo()'); + + /** @var PDO $connection */ + $connection = $I->getConnection(); + + $custIdOne = 50; + $firstNameOne = uniqid('cust-1-', true); + $lastNameOne = uniqid('cust-1-', true); + + $customersMigration = new CustomersMigration($connection); + $customersMigration->insert($custIdOne, 0, $firstNameOne, $lastNameOne); + + $invoiceId = 50; + $title = uniqid('inv-'); + $invoicesMigration = new InvoicesMigration($connection); + $invoicesMigration->insert( + $invoiceId, + $custIdOne, + Invoices::STATUS_PAID, + $title . '-paid' + ); + $invoiceId = 70; + $title = uniqid('inv-'); + $invoicesMigration->insert( + $invoiceId, + $custIdOne, + 0, + $title . '' + ); + + $invoice = Invoices::findFirst(50); + $actual = $invoice->customer; + $I->assertNotNull($actual); + + $actual = $invoice->customerMultipleFields; + $I->assertNull($actual); + + $invoice = Invoices::findFirst(70); + $actual = $invoice->customer; + $I->assertNotNull($actual); + + $actual = $invoice->customerMultipleFields; + $I->assertNotNull($actual); + } + + /** + * Tests Phalcon\Mvc\Model :: HasMany() - get + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + * @group sqlite + */ + public function mvcModelGetHasMany(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - get - HasMany()'); + + /** @var PDO $connection */ + $connection = $I->getConnection(); + + + $custIdOne = 50; + $firstNameOne = uniqid('cust-1-', true); + $lastNameOne = uniqid('cust-1-', true); + + $customersMigration = new CustomersMigration($connection); + $customersMigration->insert($custIdOne, 0, $firstNameOne, $lastNameOne); + + $invoiceId = 50; + $title = uniqid('inv-'); + $invoicesMigration = new InvoicesMigration($connection); + $invoicesMigration->insert( + $invoiceId, + $custIdOne, + Invoices::STATUS_PAID, + $title . '-paid' + ); + $invoiceId = 70; + $title = uniqid('inv-'); + $invoicesMigration->insert( + $invoiceId, + $custIdOne, + 0, + $title . '' + ); + + $customer = Customers::findFirst(50); + + $invoices = $customer->getRelated('invoices'); + $actual = count($invoices); + $expected = 2; + $I->assertEquals($expected, $actual); + + $invoices = $customer->getRelated('invoicesMultipleFields'); + $actual = count($invoices); + $expected = 1; + $I->assertEquals($expected, $actual); + + $invoice = $invoices->getFirst(); + $expected = 0; + $actual = $invoice->inv_status_flag; + + $I->assertEquals($expected, $actual); + + $invoices = $customer->getRelated('invoices'); + $actual = count($invoices); + $expected = 2; + $I->assertEquals($expected, $actual); + $invoice = $invoices->getFirst(); + $expected = 1; + $actual = $invoice->inv_status_flag; + + $I->assertEquals($expected, $actual); + } + + + /** + * Tests Phalcon\Mvc\Model :: hasOneThrough() - get + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + */ + public function mvcModelGetHasOneThrough(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - get - HasOneThrough()'); + + /** @var PDO $connection */ + $connection = $I->getConnection(); + + $orderId = 10; + $orderName = uniqid('ord', true); + $orderStatus = 5; + $productId = 20; + $productName = uniqid('prd', true); + $productStatus = 10; + $quantity = 1; + + $ordersMigragion = new OrdersMigration($connection); + $ordersProductsMigration = new OrdersProductsMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsMigration->insert($orderId, $productId, $quantity, 0, 0); + + $productId = 30; + $productName = uniqid('prd-2-', true); + $productStatus = 10; + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsMigration->insert($orderId, $productId, $quantity, $orderStatus, $productStatus); + + $orders = OrdersMultiple::findFirst(10); + + $product = $orders->singleProduct; + $expected = 20; + $actual = $product->prd_id; + $I->assertEquals($expected, $actual); + + $product = $orders->singleProductMultipleFields; + $expected = 30; + $actual = $product->prd_id; + $I->assertEquals($expected, $actual); + } + + /** + * Tests Phalcon\Mvc\Model :: hasManytoMany() - get + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + */ + public function mvcModelGetHasManyToMany(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - get - HasManyToMany()'); + + /** @var PDO $connection */ + $connection = $I->getConnection(); + + $orderId = 10; + $orderName = uniqid('ord', true); + $orderStatus = 5; + $productId = 20; + $productName = uniqid('prd', true); + $productStatus = 10; + $quantity = 1; + + $ordersMigragion = new OrdersMigration($connection); + $ordersProductsMigration = new OrdersProductsMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsMigration->insert($orderId, $productId, $quantity, 0, 0); + + $productId = 30; + $productName = uniqid('prd-2-', true); + $productStatus = 10; + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsMigration->insert($orderId, $productId, $quantity, $orderStatus, $productStatus); + + $orders = OrdersMultiple::findFirst(10); + + $products = $orders->products; + $expected = 2; + $actual = count($products); + $I->assertEquals($expected, $actual); + + $products = $orders->productsMultipleFields; + $expected = 1; + $actual = count($products); + $I->assertEquals($expected, $actual); + + $product = $products->getFirst(); + $expected = 30; + $actual = $product->prd_id; + $I->assertEquals($expected, $actual); + } + + /** + * Tests Phalcon\Mvc\Model :: BelongsTo() - set + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + * @group sqlite + */ + public function mvcModelSetBelongsTo(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - set - BelongsTo()'); + + /** @var PDO $connection */ + $connection = $I->getConnection(); + + + $custIdOne = 50; + $firstNameOne = uniqid('cust-1-', true); + $lastNameOne = uniqid('cust-1-', true); + + $customersMigration = new CustomersMigration($connection); + $customersMigration->insert($custIdOne, 0, $firstNameOne, $lastNameOne); + + $invoiceId = 50; + $title = uniqid('inv-'); + $invoicesMigration = new InvoicesMigration($connection); + $invoicesMigration->insert( + $invoiceId, + $custIdOne, + Invoices::STATUS_PAID, + $title . '-paid' + ); + $customer = Customers::findFirst(50); + + $invoice = new Invoices(); + $invoice->inv_id = 70; + $invoice->inv_title = $title = uniqid('inv-'); + + $invoice->customerMultipleFields = $customer; + + $actual = $invoice->save(); + + $I->assertTrue($actual); + + $expected = 0; + $actual = $invoice->inv_status_flag; + $I->assertEquals($expected, $actual); + + $expected = 50; + $actual = $invoice->inv_cst_id; + $I->assertEquals($expected, $actual); + + $expected = 0; + $actual = $invoice->getDirtyState(); + $I->assertEquals($expected, $actual); + } + + /** + * Tests Phalcon\Mvc\Model :: hasMany() - set + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + * @group sqlite + */ + public function mvcModelSetHasMany(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - set - HasMany()'); + + /** @var PDO $connection */ + $connection = $I->getConnection(); + + $custIdOne = 50; + $firstNameOne = uniqid('cust-1-', true); + $lastNameOne = uniqid('cust-1-', true); + + $customersMigration = new CustomersMigration($connection); + $customersMigration->insert($custIdOne, 0, $firstNameOne, $lastNameOne); + + $invoiceId = 50; + $title = uniqid('inv-'); + $invoicesMigration = new InvoicesMigration($connection); + $invoicesMigration->insert( + $invoiceId, + $custIdOne, + Invoices::STATUS_PAID, + $title . '-paid' + ); + $customer = Customers::findFirst(50); + + $invoice = new Invoices(); + $invoice->inv_id = 70; + $invoice->inv_title = $title = uniqid('inv-'); + + + $customer->InvoicesMultipleFields = [$invoice]; + $actual = $customer->save(); + + $I->assertTrue($actual); + + $expected = 0; + $actual = $invoice->inv_status_flag; + $I->assertEquals($expected, $actual); + + $expected = 50; + $actual = $invoice->inv_cst_id; + $I->assertEquals($expected, $actual); + + $expected = 0; + $actual = $invoice->getDirtyState(); + $I->assertEquals($expected, $actual); + } + + /** + * Tests Phalcon\Mvc\Model :: hasOneThrough() - set + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + */ + public function mvcModelSetHasOneThrough(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - set - HasOneThrough()'); + + /** @var PDO $connection */ + $connection = $I->getConnection(); + + $orderId = 10; + $orderName = uniqid('ord', true); + $orderStatus = 5; + + $ordersMigragion = new OrdersMigration($connection); + $ordersProductsMigration = new OrdersProductsMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + + $orders = OrdersMultiple::findFirst(10); + $product = new Products(); + $product->prd_id = 20; + $product->prd_name = uniqid('prd', true); + $product->prd_status_flag = 0; + + $orders->singleProduct = $product; + $actual = $orders->save(); + + $I->assertTrue($actual); + + $expected = 0; + $actual = $product->getDirtyState(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProducts::find(); + $expected = 1; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $product = new Products(); + $product->prd_id = 30; + $product->prd_name = uniqid('prd2', true); + $product->prd_status_flag = 10; + + $orders->singleProductMultipleFields = $product; + $actual = $orders->save(); + $I->assertTrue($actual); + + $expected = 0; + $actual = $product->getDirtyState(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProducts::find(); + $expected = 2; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + } + + /** + * Tests Phalcon\Mvc\Model :: hasManyToMany() - set + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + */ + public function mvcModelSetHasManyToMany(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - set - HasManyToMany()'); + + /** @var PDO $connection */ + $connection = $I->getConnection(); + + $orderId = 10; + $orderName = uniqid('ord', true); + $orderStatus = 5; + + $ordersMigragion = new OrdersMigration($connection); + $ordersProductsMigration = new OrdersProductsMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + + $orders = OrdersMultiple::findFirst(10); + $product1 = new Products(); + $product1->prd_id = 20; + $product1->prd_name = uniqid('prd', true); + $product1->prd_status_flag = 0; + + $orders->products = [$product1]; + $actual = $orders->save(); + $I->assertTrue($actual); + + $expected = 0; + $actual = $product1->getDirtyState(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProducts::find(); + $expected = 1; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $product2 = new Products(); + $product2->prd_id = 30; + $product2->prd_name = uniqid('prd2', true); + $product2->prd_status_flag = 10; + + $orders->productsMultipleFields = [$product2]; + $actual = $orders->save(); + $I->assertTrue($actual); + + $expected = 0; + $actual = $product2->getDirtyState(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProducts::find(); + $expected = 2; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $orders->productsMultipleFields = [$product1, $product2]; + $actual = $orders->save(); + $I->assertTrue($actual); + + $intermidiate = OrdersProducts::find(); + $expected = 2; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $products = $orders->getRelated('productsMultipleFields'); + $expected = 2; + $actual = count($products); + $I->assertEquals($expected, $actual); + } +}