diff --git a/CHANGELOG-5.0.md b/CHANGELOG-5.0.md index e972304018..1900b44b39 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 e5f5bf9b9a..e58aeade85 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, j, t, h; + string intermediateConditions; + array conditions, placeholders, placeholdersMany; 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,38 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, let intermediateModelName = relation->getIntermediateModel(), intermediateFields = relation->getIntermediateFields(), intermediateReferencedFields = relation->getIntermediateReferencedFields(); + let placeholders = []; + let conditions = []; + /** + * Always check for existing intermediate models + * otherwise conflicts will arise on insert instead of update + */ + if unlikely typeof columns === "array" { + let columnCount = count(columns) - 1; + for i in range(0, columnCount) { + let columnA = columns[i]; + let conditions[] = "[". intermediateFields[i] . "] = :APR" . i . ":"; + let placeholders["APR" . i] = this->{columnA}; + } + let i = columnCount + 1; + } else { + let conditions[] = "[" . intermediateFields . "] = :APR0:"; + let placeholders["APR0"] = this->{columns}; + let i = 1; + } + if relation->getType() === Relation::HAS_MANY_THROUGH { + if unlikely typeof referencedFields === "array" { + let referencedFieldsCount = count(referencedFields) - 1; + for j in range(0, referencedFieldsCount) { + let t = j + i; + let conditions[] = "[". intermediateReferencedFields[j] . "] = :APR" . t . ":"; + } + } else { + let conditions[] = "[". intermediateReferencedFields . "] = :APR" . i . ":"; + } + } + let intermediateConditions = join(" AND ", conditions); for recordAfter in relatedRecords { /** * Save the record and get messages @@ -5191,14 +5207,25 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, * referenced model */ this->appendMessagesFrom(recordAfter); - + /** * Rollback the implicit transaction */ connection->rollback(nesting); - + return false; } + if relation->getType() === Relation::HAS_MANY_THROUGH { + if unlikely typeof referencedFields === "array" { + for j in range(0, referencedFieldsCount) { + let columnA = referencedFields[j]; + let t = j + i; + let placeholders["APR" . t] = recordAfter->{columnA}; + } + } else { + let placeholders["APR" . i] = recordAfter->{referencedFields}; + } + } /** * Create a new instance of the intermediate model */ @@ -5207,45 +5234,43 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface, ); /** - * Has-one-through relations can only use one intermediate model. * If it already exist, it can be updated with the new referenced key. */ - if relation->getType() == Relation::HAS_ONE_THROUGH { - let existingIntermediateModel = intermediateModel->findFirst( - [ - "[" . intermediateFields . "] = ?0", - "bind": [value] - ] - ); + let existingIntermediateModel = intermediateModel->findFirst( + [ + intermediateConditions, + "bind": placeholders + ] + ); - if existingIntermediateModel { - let intermediateModel = existingIntermediateModel; + if existingIntermediateModel { + let intermediateModel = existingIntermediateModel; + } + if !existingIntermediateModel || relation->getType() === Relation::HAS_ONE_THROUGH { + /** + * Write value in the intermediate model + */ + if unlikely typeof columns === "array" { + for h in range(0, columnCount) { + let columnA = columns[h]; + let columnB = intermediateFields[h]; + let intermediateModel->{columnB} = this->{columnA}; + } + } else { + let intermediateModel->{intermediateFields} = this->{columns}; + } + if unlikely typeof referencedFields === "array" { + let referencedFieldsCount = count(referencedFields) - 1; + for h in range(0, referencedFieldsCount) { + let columnA = referencedFields[h]; + let columnB = intermediateReferencedFields[h]; + let intermediateModel->{columnB} = recordAfter->{columnA}; + } + } else { + let intermediateModel->{intermediateReferencedFields} = recordAfter->{referencedFields}; } } - /** - * 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 - ); - /** * Save the record and get messages */ @@ -5264,27 +5289,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 99cee971ae..2e1b740b2a 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 dbd9f6dbf3..057f292998 100644 --- a/tests/_data/assets/schemas/mysql.sql +++ b/tests/_data/assets/schemas/mysql.sql @@ -189,21 +189,69 @@ 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; +drop table if exists `co_orders_x_products_one`; + +CREATE TABLE `co_orders_x_products_one` ( + `oxp_id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `oxp_ord_id` int(10) unsigned NOT NULL, + `oxp_prd_id` int(10) unsigned NOT NULL, + `oxp_quantity` int(10) unsigned NULL, + PRIMARY KEY (`oxp_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +drop table if exists `co_orders_x_products_mult`; + +CREATE TABLE `co_orders_x_products_mult` ( + `oxp_id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `oxp_ord_id` int(10) unsigned NOT NULL, + `oxp_ord_status_flag` tinyint(1) NOT NULL, + `oxp_prd_id` int(10) unsigned NOT NULL, + `oxp_prd_status_flag` tinyint(1) NOT NULL, + `oxp_quantity` int(10) unsigned NULL, + PRIMARY KEY (`oxp_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +drop table if exists `co_orders_x_products_one_comp`; + +CREATE TABLE `co_orders_x_products_one_comp` ( + `oxp_ord_id` int(10) unsigned NOT NULL, + `oxp_prd_id` int(10) unsigned NOT NULL, + `oxp_quantity` int(10) unsigned NULL, + PRIMARY KEY (`oxp_ord_id`, `oxp_prd_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +drop table if exists `co_orders_x_products_mult_comp`; + +CREATE TABLE `co_orders_x_products_mult_comp` ( + `oxp_ord_id` int(10) unsigned NOT NULL, + `oxp_ord_status_flag` tinyint(1) NOT NULL, + `oxp_prd_id` int(10) unsigned NOT NULL, + `oxp_prd_status_flag` tinyint(1) NOT NULL, + `oxp_quantity` int(10) unsigned NULL, + PRIMARY KEY (`oxp_ord_id`, `oxp_prd_id`, `oxp_ord_status_flag`, `oxp_prd_status_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + drop table if exists `photo`; @@ -235,6 +283,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 8f7afc2489..a76fe16cd4 100644 --- a/tests/_data/assets/schemas/pgsql.sql +++ b/tests/_data/assets/schemas/pgsql.sql @@ -113,35 +113,69 @@ drop table if exists co_orders; create table co_orders ( - ord_id serial not null - constraint ord_pk - primary key, - ord_name varchar(70) + ord_id serial not null constraint co_ord_pk primary key, + 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_one; -create table private.co_orders_x_products +create table co_orders_x_products_one ( + oxp_id serial not null constraint co_orders_x_products_one_pk primary key, oxp_ord_id int not null, oxp_prd_id int not null, - oxp_quantity int not null + oxp_quantity int null ); +drop table if exists co_orders_x_products_mult; + +create table co_orders_x_products_mult +( + oxp_id serial not null constraint co_orders_x_products_mult_pk primary key, + oxp_ord_id int not null, + oxp_prd_id int not null, + oxp_quantity int null, + oxp_ord_status_flag integer, + oxp_prd_status_flag integer +); + +drop table if exists co_orders_x_products_one_comp; + +create table co_orders_x_products_one_comp +( + oxp_ord_id int not null, + oxp_prd_id int not null, + oxp_quantity int null, + primary key (oxp_ord_id, oxp_prd_id) +); + + +drop table if exists co_orders_x_products_mult_comp; + +create table co_orders_x_products_mult_comp +( + oxp_ord_id int not null, + oxp_prd_id int not null, + oxp_quantity int null, + oxp_ord_status_flag integer, + oxp_prd_status_flag integer, + primary key (oxp_ord_id, oxp_prd_id, oxp_ord_status_flag, oxp_prd_status_flag) +); + drop table if exists co_products; create table co_products ( - prd_id serial not null - constraint prd_pk - primary key, - prd_name varchar(70) + prd_id serial constraint co_prd_pk primary key, + 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 d7a8198922..a76707060e 100644 --- a/tests/_data/assets/schemas/sqlite.sql +++ b/tests/_data/assets/schemas/sqlite.sql @@ -139,3 +139,76 @@ 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_prd_id` integer NOT NULL, + `oxp_quantity` integer NULL, + PRIMARY KEY (`oxp_ord_id`, `oxp_prd_id`) +); + + +drop table if exists co_orders_x_products_one; + +create table co_orders_x_products_one ( + `oxp_id` integer constraint co_orders_x_products_one_pk primary key autoincrement, + `oxp_ord_id` integer NOT NULL, + `oxp_prd_id` integer NOT NULL, + `oxp_quantity` integer NULL +); + + + +drop table if exists co_orders_x_products_mult; + +create table co_orders_x_products_mult ( + `oxp_id` integer constraint co_orders_x_products_mult_pk primary key autoincrement, + `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 +); + +drop table if exists co_orders_x_products_one_comp; + +create table co_orders_x_products_one_comp ( + `oxp_ord_id` integer NOT NULL, + `oxp_prd_id` integer NOT NULL, + `oxp_quantity` integer NULL, + PRIMARY KEY (`oxp_ord_id`, `oxp_prd_id`) +); + + +drop table if exists co_orders_x_products_mult_comp; + +create table co_orders_x_products_mult_comp ( + `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, + PRIMARY KEY (`oxp_ord_id`, `oxp_prd_id`, `oxp_ord_status_flag`, `oxp_prd_status_flag`) +); diff --git a/tests/_data/fixtures/Migrations/OrdersMigration.php b/tests/_data/fixtures/Migrations/OrdersMigration.php index b9dbe6a7a3..5f677291a8 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 = << + * + * 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\Fixtures\Migrations; + +/** + * Class OrdersProductsFieldsMultCompMigration + */ +class OrdersProductsFieldsMultCompMigration extends AbstractMigration +{ + protected $table = "co_orders_x_products_mult_comp"; + + /** + * @param int $oxp_ord_id + * @param int $oxp_prd_id + * + * @return int + */ + public function insert( + int $oxp_ord_id, + int $oxp_prd_id, + int $oxp_quantity, + int $oxp_ord_status_flag = 0, + int $oxp_prd_status_flag = 0 + ): int { + $oxp_ord_id = $oxp_ord_id ?: 'null'; + $oxp_prd_id = $oxp_prd_id ?: 'null'; + $oxp_quantity = $oxp_quantity ?: 'null'; + $oxp_ord_status_flag = $oxp_ord_status_flag ?: 0; + $oxp_prd_status_flag = $oxp_prd_status_flag ?: 0; + $sql = <<connection->exec($sql); + } + + protected function getSqlMysql(): array + { + return [ + " +drop table if exists `co_orders_x_products_mult_comp`; + ", + " +CREATE TABLE `co_orders_x_products_mult_comp` ( + `oxp_ord_id` int(10) unsigned NOT NULL, + `oxp_ord_status_flag` tinyint(1) NOT NULL, + `oxp_prd_id` int(10) unsigned NOT NULL, + `oxp_prd_status_flag` tinyint(1) NOT NULL, + `oxp_quantity` int(10) unsigned NULL, + PRIMARY KEY (`oxp_ord_id`, `oxp_prd_id`, `oxp_ord_status_flag`, `oxp_prd_status_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + " + ]; + } + + protected function getSqlSqlite(): array + { + return [ +"drop table if exists co_orders_x_products_mult_comp;", +"create table co_orders_x_products_mult_comp ( + `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, + primary key (`oxp_ord_id`, `oxp_prd_id`, `oxp_ord_status_flag`, `oxp_prd_status_flag`) +);" + ]; + } + + protected function getSqlPgsql(): array + { + return [ + " +drop table if exists co_orders_x_products_mult_comp; + ", + " +create table co_orders_x_products_mult_comp +( + oxp_ord_id int not null, + oxp_prd_id int not null, + oxp_quantity int null, + oxp_ord_status_flag integer, + oxp_prd_status_flag integer, + primary key (oxp_ord_id, oxp_prd_id, oxp_ord_status_flag, oxp_prd_status_flag) +); + " + ]; + } + + protected function getSqlSqlsrv(): array + { + return []; + } +} diff --git a/tests/_data/fixtures/Migrations/OrdersProductsFieldsMultMigration.php b/tests/_data/fixtures/Migrations/OrdersProductsFieldsMultMigration.php new file mode 100644 index 0000000000..be257714a2 --- /dev/null +++ b/tests/_data/fixtures/Migrations/OrdersProductsFieldsMultMigration.php @@ -0,0 +1,113 @@ + + * + * 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\Fixtures\Migrations; + +/** + * Class OrdersProductsMigration + */ +class OrdersProductsFieldsMultMigration extends AbstractMigration +{ + protected $table = "co_orders_x_products_mult"; + + /** + * @param int $oxp_ord_id + * @param int $oxp_prd_id + * + * @return int + */ + public function insert( + int $oxp_id, + int $oxp_ord_id, + int $oxp_prd_id, + int $oxp_quantity, + int $oxp_ord_status_flag = 0, + int $oxp_prd_status_flag = 0 + ): int { + $oxp_id = $oxp_id ?: 'null'; + $oxp_ord_id = $oxp_ord_id ?: 'null'; + $oxp_prd_id = $oxp_prd_id ?: 'null'; + $oxp_quantity = $oxp_quantity ?: 'null'; + $oxp_ord_status_flag = $oxp_ord_status_flag ?: 0; + $oxp_prd_status_flag = $oxp_prd_status_flag ?: 0; + $sql = <<connection->exec($sql); + } + + protected function getSqlMysql(): array + { + return [ + " +drop table if exists `co_orders_x_products_mult`; + ", + " +CREATE TABLE `co_orders_x_products_mult` ( + `oxp_id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `oxp_ord_id` int(10) unsigned NOT NULL, + `oxp_ord_status_flag` tinyint(1) NOT NULL, + `oxp_prd_id` int(10) unsigned NOT NULL, + `oxp_prd_status_flag` tinyint(1) NOT NULL, + `oxp_quantity` int(10) unsigned NULL, + PRIMARY KEY (`oxp_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + " + ]; + } + + protected function getSqlSqlite(): array + { + return [ +"drop table if exists co_orders_x_products_mult;", +"create table co_orders_x_products_mult ( + `oxp_id` integer constraint ord_id_pk primary key autoincrement, + `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 +);" + ]; + } + + protected function getSqlPgsql(): array + { + return [ + " +drop table if exists co_orders_x_products_mult; + ", + " +create table co_orders_x_products_mult +( + oxp_id serial not null constraint ord_pk primary key + oxp_ord_id int not null, + oxp_prd_id int not null, + oxp_quantity int null, + oxp_ord_status_flag integer, + oxp_prd_status_flag integer +); + " + ]; + } + + protected function getSqlSqlsrv(): array + { + return []; + } +} diff --git a/tests/_data/fixtures/Migrations/OrdersProductsFieldsOneCompMigration.php b/tests/_data/fixtures/Migrations/OrdersProductsFieldsOneCompMigration.php new file mode 100644 index 0000000000..530cd8b158 --- /dev/null +++ b/tests/_data/fixtures/Migrations/OrdersProductsFieldsOneCompMigration.php @@ -0,0 +1,100 @@ + + * + * 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\Fixtures\Migrations; + +/** + * Class OrdersProductsFieldsOneCompMigration + */ +class OrdersProductsFieldsOneCompMigration extends AbstractMigration +{ + protected $table = "co_orders_x_products_one_comp"; + + /** + * @param int $oxp_ord_id + * @param int $oxp_prd_id + * + * @return int + */ + public function insert( + int $oxp_ord_id, + int $oxp_prd_id, + int $oxp_quantity + ): int { + $oxp_ord_id = $oxp_ord_id ?: 'null'; + $oxp_prd_id = $oxp_prd_id ?: 'null'; + $oxp_quantity = $oxp_quantity ?: 'null'; + $sql = <<connection->exec($sql); + } + + protected function getSqlMysql(): array + { + return [ + " +drop table if exists `co_orders_x_products_one_comp`; + ", + " +CREATE TABLE `co_orders_x_products_one_comp` ( + `oxp_ord_id` int(10) unsigned NOT NULL, + `oxp_prd_id` int(10) unsigned NOT NULL, + `oxp_quantity` int(10) unsigned NULL, + PRIMARY KEY (`oxp_ord_id`, `oxp_prd_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + " + ]; + } + + protected function getSqlSqlite(): array + { + return [ +"drop table if exists co_orders_x_products_one_comp;", +"create table co_orders_x_products_one_comp ( + `oxp_ord_id` integer NOT NULL, + `oxp_prd_id` integer NOT NULL, + `oxp_quantity` integer NULL, + primary key (`oxp_ord_id`, `oxp_prd_id`) +);" + ]; + } + + protected function getSqlPgsql(): array + { + return [ + " +drop table if exists co_orders_x_products_one_comp; + ", + " +create table co_orders_x_products_one_comp +( + oxp_ord_id int not null, + oxp_prd_id int not null, + oxp_quantity int null, + primary key (oxp_ord_id, oxp_prd_id) +); + " + ]; + } + + protected function getSqlSqlsrv(): array + { + return []; + } +} diff --git a/tests/_data/fixtures/Migrations/OrdersProductsFieldsOneMigration.php b/tests/_data/fixtures/Migrations/OrdersProductsFieldsOneMigration.php new file mode 100644 index 0000000000..fb191d3265 --- /dev/null +++ b/tests/_data/fixtures/Migrations/OrdersProductsFieldsOneMigration.php @@ -0,0 +1,103 @@ + + * + * 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\Fixtures\Migrations; + +/** + * Class OrdersProductsMigration + */ +class OrdersProductsFieldsOneMigration extends AbstractMigration +{ + protected $table = "co_orders_x_products_one"; + + /** + * @param int $oxp_ord_id + * @param int $oxp_prd_id + * + * @return int + */ + public function insert( + int $oxp_id, + int $oxp_ord_id, + int $oxp_prd_id, + int $oxp_quantity + ): int { + $oxp_id = $oxp_id ?: 'null'; + $oxp_ord_id = $oxp_ord_id ?: 'null'; + $oxp_prd_id = $oxp_prd_id ?: 'null'; + $oxp_quantity = $oxp_quantity ?: 'null'; + $sql = <<connection->exec($sql); + } + + protected function getSqlMysql(): array + { + return [ + " +drop table if exists `co_orders_x_products_one`; + ", + " +CREATE TABLE `co_orders_x_products_one` ( + `oxp_id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `oxp_ord_id` int(10) unsigned NOT NULL, + `oxp_prd_id` int(10) unsigned NOT NULL, + `oxp_quantity` int(10) unsigned NULL, + PRIMARY KEY (`oxp_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + " + ]; + } + + protected function getSqlSqlite(): array + { + return [ +"drop table if exists co_orders_x_products_one;", +"create table co_orders_x_products_one ( + `oxp_ord_id` integer NOT NULL, + `oxp_prd_id` integer NOT NULL, + `oxp_quantity` integer NULL, + primary key (`oxp_ord_id`) +);" + ]; + } + + protected function getSqlPgsql(): array + { + return [ + " +drop table if exists co_orders_x_products_one; + ", + " +create table co_orders_x_products_one +( + oxp_id serial not null constraint ord_pk primary key + oxp_ord_id int not null, + oxp_prd_id int not null, + oxp_quantity int null +); + " + ]; + } + + protected function getSqlSqlsrv(): array + { + return []; + } +} diff --git a/tests/_data/fixtures/Migrations/OrdersProductsMigration.php b/tests/_data/fixtures/Migrations/OrdersProductsMigration.php index a90def45bd..abf0f9e131 100644 --- a/tests/_data/fixtures/Migrations/OrdersProductsMigration.php +++ b/tests/_data/fixtures/Migrations/OrdersProductsMigration.php @@ -18,7 +18,7 @@ */ class OrdersProductsMigration extends AbstractMigration { - protected $table = "co_orders_x_products"; + protected $table = "private.co_orders_x_products"; /** * @param int $oxp_ord_id @@ -35,7 +35,7 @@ public function insert( $oxp_prd_id = $oxp_prd_id ?: 'null'; $oxp_quantity = $oxp_quantity ?: 'null'; $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 04eea93989..a535cd73f8 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 565236593c..63f6418d39 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 0000000000..bef082a388 --- /dev/null +++ b/tests/_data/fixtures/models/OrdersMultiple.php @@ -0,0 +1,133 @@ + + * + * 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 OrdersMultiple + * + * @property int $ord_id; + * @property string $ord_name; + * @property int $ord_status_flag; + */ +class OrdersMultiple extends Model +{ + public function initialize() + { + $this->setSource('co_orders'); + + /** + * Compound Primary key intermediate relations + */ + $this->hasManyToMany( + ['ord_id', 'ord_status_flag'], + OrdersProductsFieldsMultComp::class, + ['oxp_ord_id', 'oxp_ord_status_flag'], + ['oxp_prd_id', 'oxp_prd_status_flag'], + Products::class, + ['prd_id', 'prd_status_flag'], + [ + 'alias' => 'productsFieldsMultComp' + ] + ); + + $this->hasOneThrough( + ['ord_id', 'ord_status_flag'], + OrdersProductsFieldsMultComp::class, + ['oxp_ord_id', 'oxp_ord_status_flag'], + ['oxp_prd_id', 'oxp_prd_status_flag'], + Products::class, + ['prd_id', 'prd_status_flag'], + [ + 'alias' => 'singleProductFieldsMultComp' + ] + ); + + $this->hasManyToMany( + 'ord_id', + OrdersProductsFieldsOneComp::class, + 'oxp_ord_id', + 'oxp_prd_id', + Products::class, + 'prd_id', + [ + 'alias' => 'productsFieldsOneComp' + ] + ); + + $this->hasOneThrough( + 'ord_id', + OrdersProductsFieldsOneComp::class, + 'oxp_ord_id', + 'oxp_prd_id', + Products::class, + 'prd_id', + [ + 'alias' => 'singleProductFieldsOneComp' + ] + ); + + /** + * Single primary key intermediate relation + */ + $this->hasManyToMany( + ['ord_id', 'ord_status_flag'], + OrdersProductsFieldsMult::class, + ['oxp_ord_id', 'oxp_ord_status_flag'], + ['oxp_prd_id', 'oxp_prd_status_flag'], + Products::class, + ['prd_id', 'prd_status_flag'], + [ + 'alias' => 'productsFieldsMult' + ] + ); + + $this->hasOneThrough( + ['ord_id', 'ord_status_flag'], + OrdersProductsFieldsMult::class, + ['oxp_ord_id', 'oxp_ord_status_flag'], + ['oxp_prd_id', 'oxp_prd_status_flag'], + Products::class, + ['prd_id', 'prd_status_flag'], + [ + 'alias' => 'singleProductFieldsMult' + ] + ); + + $this->hasManyToMany( + 'ord_id', + OrdersProductsFieldsOne::class, + 'oxp_ord_id', + 'oxp_prd_id', + Products::class, + 'prd_id', + [ + 'alias' => 'productsFieldsOne' + ] + ); + + $this->hasOneThrough( + 'ord_id', + OrdersProductsFieldsOne::class, + 'oxp_ord_id', + 'oxp_prd_id', + Products::class, + 'prd_id', + [ + 'alias' => 'singleProductFieldsOne' + ] + ); + } +} diff --git a/tests/_data/fixtures/models/OrdersProducts.php b/tests/_data/fixtures/models/OrdersProducts.php index 6dacddf71f..6972d6559b 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/OrdersProductsFieldsMult.php b/tests/_data/fixtures/models/OrdersProductsFieldsMult.php new file mode 100644 index 0000000000..012fe42ac0 --- /dev/null +++ b/tests/_data/fixtures/models/OrdersProductsFieldsMult.php @@ -0,0 +1,40 @@ + + * + * 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 OrdersProductsFieldsMult + * + * @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 OrdersProductsFieldsMult extends Model +{ + public $oxp_id; + public $oxp_ord_id; + public $oxp_prd_id; + public $oxp_quantity; + public $oxp_ord_status_flag; + public $oxp_prd_status_flag; + + public function initialize() + { + $this->setSource('co_orders_x_products_mult'); + } +} diff --git a/tests/_data/fixtures/models/OrdersProductsFieldsMultComp.php b/tests/_data/fixtures/models/OrdersProductsFieldsMultComp.php new file mode 100644 index 0000000000..f9705143fa --- /dev/null +++ b/tests/_data/fixtures/models/OrdersProductsFieldsMultComp.php @@ -0,0 +1,40 @@ + + * + * 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 OrdersProductsFieldsMultComp + * + * @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 OrdersProductsFieldsMultComp 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() + { + $this->setSource('co_orders_x_products_mult_comp'); + } +} diff --git a/tests/_data/fixtures/models/OrdersProductsFieldsOne.php b/tests/_data/fixtures/models/OrdersProductsFieldsOne.php new file mode 100644 index 0000000000..7625278d44 --- /dev/null +++ b/tests/_data/fixtures/models/OrdersProductsFieldsOne.php @@ -0,0 +1,37 @@ + + * + * 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 class OrdersProductsFieldsOne + * + * @property int $oxp_ord_id; + * @property string $oxp_prd_id; + * @property string $oxp_quantity; + */ +class OrdersProductsFieldsOne extends Model +{ + public $oxp_id; + public $oxp_ord_id; + public $oxp_prd_id; + public $oxp_quantity; + + + public function initialize() + { + $this->setSource('co_orders_x_products_one'); + } +} diff --git a/tests/_data/fixtures/models/OrdersProductsFieldsOneComp.php b/tests/_data/fixtures/models/OrdersProductsFieldsOneComp.php new file mode 100644 index 0000000000..fb2b8ec5ac --- /dev/null +++ b/tests/_data/fixtures/models/OrdersProductsFieldsOneComp.php @@ -0,0 +1,38 @@ + + * + * 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 OrdersProductsFieldsOneComp + * + * @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 OrdersProductsFieldsOneComp extends Model +{ + public $oxp_ord_id; + public $oxp_prd_id; + public $oxp_quantity; + + + public function initialize() + { + $this->setSource('co_orders_x_products_one_comp'); + } +} diff --git a/tests/_data/fixtures/models/Products.php b/tests/_data/fixtures/models/Products.php index 25531c1f0b..33b416c2b6 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 0000000000..a21c963038 --- /dev/null +++ b/tests/database/Mvc/Model/RelationsCest.php @@ -0,0 +1,894 @@ + + * + * 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\OrdersProductsFieldsMultCompMigration; +use Phalcon\Tests\Fixtures\Migrations\OrdersProductsFieldsMultMigration; +use Phalcon\Tests\Fixtures\Migrations\OrdersProductsFieldsOneCompMigration; +use Phalcon\Tests\Fixtures\Migrations\OrdersProductsFieldsOneMigration; +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\OrdersProductsFieldsMult; +use Phalcon\Tests\Models\OrdersProductsFieldsMultComp; +use Phalcon\Tests\Models\OrdersProductsFieldsOne; +use Phalcon\Tests\Models\OrdersProductsFieldsOneComp; +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); + } + + public function _after(DatabaseTester $I) + { + $this->container['db']->close(); + } + + /** + * 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 + * @group sqlite + */ + 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); + $ordersProductsOneMigration = new OrdersProductsFieldsOneMigration($connection); + $ordersProductsMultMigration = new OrdersProductsFieldsMultMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsOneMigration->insert(1, $orderId, $productId, $quantity); + + $productId = 30; + $productName = uniqid('prd-2-', true); + $productStatus = 10; + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsMultMigration->insert(1, $orderId, $productId, $quantity, $orderStatus, $productStatus); + + $orders = OrdersMultiple::findFirst(10); + + $product = $orders->singleProductFieldsOne; + $expected = 20; + $actual = $product->prd_id; + $I->assertEquals($expected, $actual); + + $product = $orders->singleProductFieldsMult; + $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 + * @group sqlite + */ + 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); + $ordersProductsOneMigration = new OrdersProductsFieldsOneMigration($connection); + $ordersProductsMultMigration = new OrdersProductsFieldsMultMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsOneMigration->insert(1, $orderId, $productId, $quantity); + + $productId = 30; + $productName = uniqid('prd-2-', true); + $productStatus = 10; + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsMultMigration->insert(1, $orderId, $productId, $quantity, $orderStatus, $productStatus); + + $orders = OrdersMultiple::findFirst(10); + + $products = $orders->productsFieldsOne; + $expected = 1; + $actual = count($products); + $I->assertEquals($expected, $actual); + + $products = $orders->productsFieldsMult; + $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 :: hasOneThrough() - get + * Compound Primary Key Intermediate Relations + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + * @group sqlite + */ + public function mvcModelGetHasOneThroughComp(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - get - HasOneThrough() Comp'); + + /** @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); + $ordersProductsOneMigration = new OrdersProductsFieldsOneCompMigration($connection); + $ordersProductsMultMigration = new OrdersProductsFieldsMultCompMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsOneMigration->insert($orderId, $productId, $quantity); + + $productId = 30; + $productName = uniqid('prd-2-', true); + $productStatus = 10; + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsMultMigration->insert($orderId, $productId, $quantity, $orderStatus, $productStatus); + + $orders = OrdersMultiple::findFirst(10); + + $product = $orders->singleProductFieldsOneComp; + $expected = 20; + $actual = $product->prd_id; + $I->assertEquals($expected, $actual); + + $product = $orders->singleProductFieldsMultComp; + $expected = 30; + $actual = $product->prd_id; + $I->assertEquals($expected, $actual); + } + + /** + * Tests Phalcon\Mvc\Model :: hasManytoMany() - get + * Compound Primary Key Intermediate Relations + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + * @group sqlite + */ + public function mvcModelGetHasManyToManyComp(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - get - HasManyToMany() Comp'); + + /** @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); + $ordersProductsOneMigration = new OrdersProductsFieldsOneCompMigration($connection); + $ordersProductsMultMigration = new OrdersProductsFieldsMultCompMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsOneMigration->insert($orderId, $productId, $quantity); + + $productId = 30; + $productName = uniqid('prd-2-', true); + $productStatus = 10; + $productsMigrations->insert($productId, $productName, $productStatus); + $ordersProductsMultMigration->insert($orderId, $productId, $quantity, $orderStatus, $productStatus); + + $orders = OrdersMultiple::findFirst(10); + + $products = $orders->productsFieldsOneComp; + $expected = 1; + $actual = count($products); + $I->assertEquals($expected, $actual); + + $products = $orders->productsFieldsMultComp; + $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 + * @group sqlite + */ + 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); + $ordersProductsOneMigration = new OrdersProductsFieldsOneCompMigration($connection); + $ordersProductsMultMigration = new OrdersProductsFieldsMultCompMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + + $orders = OrdersMultiple::findFirst(10); + $product = new Products(); + $product->prd_name = uniqid('prd', true); + $product->prd_status_flag = 0; + + $orders->singleProductFieldsOneComp = $product; + $actual = $orders->save(); + + $I->assertTrue($actual); + + $expected = 0; + $actual = $product->getDirtyState(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProductsFieldsOneComp::find(); + $expected = 1; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $product = new Products(); + $product->prd_name = uniqid('prd2', true); + $product->prd_status_flag = 10; + + + $orders->singleProductFieldsOneComp = $product; + $actual = $orders->save(); + $I->assertTrue($actual); + + $intermidiate = OrdersProductsFieldsOneComp::find(); + $expected = 1; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $orders->singleProductFieldsMultComp = $product; + $actual = $orders->save(); + $I->assertTrue($actual); + + $expected = 0; + $actual = $product->getDirtyState(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProductsFieldsMultComp::find(); + $expected = 1; + $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 + * @group sqlite + */ + 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); + $ordersProductsOneMigration = new OrdersProductsFieldsOneMigration($connection); + $ordersProductsMultMigration = new OrdersProductsFieldsMultMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + + $orders = OrdersMultiple::findFirst(10); + $product1 = new Products(); + $product1->prd_name = uniqid('prd1', true); + $product1->prd_status_flag = 5; + + $product2 = new Products(); + $product2->prd_name = uniqid('prd2', true); + $product2->prd_status_flag = 10; + + $orders->productsFieldsOne = [$product1]; + $actual = $orders->save(); + $I->assertTrue($actual); + + $expected = 0; + $actual = $product1->getDirtyState(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProductsFieldsOne::find(); + $expected = 1; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $orders->productsFieldsOne = [$product1, $product2]; + $actual = $orders->save(); + $I->assertTrue($actual); + + $expected = 0; + $actual = $product2->getDirtyState(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProductsFieldsOne::find(); + $expected = 2; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $products = Products::find(); + $expected = 2; + $actual = $products->count(); + $I->assertEquals($expected, $actual); + + /** + * Multiple Keys + */ + $orders->productsFieldsMult = [$product1]; + $actual = $orders->save(); + $I->assertTrue($actual); + + $intermidiate = OrdersProductsFieldsMult::find(); + $expected = 1; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $orders->productsFieldsMult = [$product2]; + $actual = $orders->save(); + $I->assertTrue($actual); + + $intermidiate = OrdersProductsFieldsMult::find(); + $expected = 2; + $actual = $intermidiate->count(); + $I->assertEquals($expected, $actual); + + $orders->productsFieldsMult = [$product1, $product2]; + $actual = $orders->save(); + $I->assertTrue($actual); + + $intermidiate = OrdersProductsFieldsMult::find(); + $expected = 2; + $actual = $intermidiate->count(); + $I->assertEquals($expected, $actual); + + $products = $orders->getRelated('productsFieldsMult'); + $expected = 2; + $actual = count($products); + $I->assertEquals($expected, $actual); + } + + /** + * Tests Phalcon\Mvc\Model :: hasOneThrough() - set + * Compound Primary Key Intermediate Relations + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + * @group sqlite + */ + public function mvcModelSetHasOneThroughComp(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - set - HasOneThrough() Comp'); + + /** @var PDO $connection */ + $connection = $I->getConnection(); + + $orderId = 10; + $orderName = uniqid('ord', true); + $orderStatus = 5; + + $ordersMigragion = new OrdersMigration($connection); + $ordersProductsOneMigration = new OrdersProductsFieldsOneCompMigration($connection); + $ordersProductsMultMigration = new OrdersProductsFieldsMultCompMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + + $orders = OrdersMultiple::findFirst(10); + $product = new Products(); + $product->prd_name = uniqid('prd', true); + $product->prd_status_flag = 0; + + $orders->singleProductFieldsOneComp = $product; + $actual = $orders->save(); + + $I->assertTrue($actual); + + $expected = 0; + $actual = $product->getDirtyState(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProductsFieldsOneComp::find(); + $expected = 1; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $product = new Products(); + $product->prd_name = uniqid('prd2', true); + $product->prd_status_flag = 10; + + + $orders->singleProductFieldsOneComp = $product; + $actual = $orders->save(); + $I->assertTrue($actual); + + $intermidiate = OrdersProductsFieldsOneComp::find(); + $expected = 1; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $orders->singleProductFieldsMultComp = $product; + $actual = $orders->save(); + $I->assertTrue($actual); + + $expected = 0; + $actual = $product->getDirtyState(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProductsFieldsMultComp::find(); + $expected = 1; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + } + + /** + * Tests Phalcon\Mvc\Model :: hasManyToMany() - set + * Compound Primary Key Intermediate Relations + * + * @param DatabaseTester $I + * + * @author Phalcon Team + * @since 2023-08-16 + * + * @group mysql + * @group pgsql + * @group sqlite + */ + public function mvcModelSetHasManyToManyComp(DatabaseTester $I) + { + $I->wantToTest('Mvc\Model - set - HasManyToMany() Comp'); + + /** @var PDO $connection */ + $connection = $I->getConnection(); + + $orderId = 10; + $orderName = uniqid('ord', true); + $orderStatus = 5; + + $ordersMigragion = new OrdersMigration($connection); + $ordersProductsOneMigration = new OrdersProductsFieldsOneCompMigration($connection); + $ordersProductsMultMigration = new OrdersProductsFieldsMultCompMigration($connection); + $productsMigrations = new ProductsMigration($connection); + + $ordersMigragion->insert($orderId, $orderName, $orderStatus); + + $orders = OrdersMultiple::findFirst(10); + $product1 = new Products(); + $product1->prd_name = uniqid('prd', true); + $product1->prd_status_flag = 0; + + $orders->productsFieldsOneComp = [$product1]; + $actual = $orders->save(); + $I->assertTrue($actual); + + $expected = 0; + $actual = $product1->getDirtyState(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProductsFieldsOneComp::find(); + $expected = 1; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $product2 = new Products(); + $product2->prd_name = uniqid('prd2', true); + $product2->prd_status_flag = 10; + + $orders->productsFieldsMultComp = [$product2]; + $actual = $orders->save(); + $I->assertTrue($actual); + + + $expected = 0; + $actual = $product2->getDirtyState(); + $I->assertEquals($expected, $actual); + + $products = Products::find(); + $expected = 2; + $actual = $products->count(); + $I->assertEquals($expected, $actual); + + $intermidiate = OrdersProductsFieldsMultComp::find(); + $expected = 1; + $actual = count($intermidiate); + $I->assertEquals($expected, $actual); + + $orders->productsFieldsMultComp = [$product1, $product2]; + $actual = $orders->save(); + $I->assertTrue($actual); + + $intermidiate = OrdersProductsFieldsMultComp::find(); + $expected = 2; + $actual = $intermidiate->count(); + $I->assertEquals($expected, $actual); + + $products = $orders->getRelated('productsFieldsMultComp'); + $expected = 2; + $actual = count($products); + $I->assertEquals($expected, $actual); + } +}