From f649e4a2ebe3dc443b5e394e13cee93b8e3fe8e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 14 Jan 2022 23:19:35 +0100 Subject: [PATCH 01/16] fix makeFakeModelWithForeignTable --- src/Persistence/Array_/Join.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Persistence/Array_/Join.php b/src/Persistence/Array_/Join.php index 4d2fcd3db..900d1b201 100644 --- a/src/Persistence/Array_/Join.php +++ b/src/Persistence/Array_/Join.php @@ -38,9 +38,17 @@ protected function makeFakeModelWithForeignTable(): Model $this->getOwner()->assertIsModel(); $modelCloned = clone $this->getOwner(); + foreach ($modelCloned->getFields() as $field) { + if ($field->hasJoin() && $field->getJoin()->foreign_table === $this->foreign_table) { + \Closure::bind(fn () => $field->joinName = null, null, \Atk4\Data\Field::class)(); + } else { + $modelCloned->removeField($field->short_name); + } + } + $modelCloned->addField($this->id_field, ['type' => 'integer']); $modelCloned->table = $this->foreign_table; - // @TODO hooks will be fixed on a cloned model, Join should be replaced later by supporting unioned table as a table model + // @TODO hooks will be fixed on a cloned model, foreign_table string name should be replaced with object model return $modelCloned; } From 73a6a61ea292ede13d9ec5611aba99bd0d7a55c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Fri, 14 Jan 2022 23:51:12 +0100 Subject: [PATCH 02/16] fix last insert ID fetch for Join for PostgreSQL --- src/Persistence/Sql/Join.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Persistence/Sql/Join.php b/src/Persistence/Sql/Join.php index fdb066f42..8a0ed7e53 100644 --- a/src/Persistence/Sql/Join.php +++ b/src/Persistence/Sql/Join.php @@ -168,7 +168,12 @@ public function afterInsert(Model $entity): void $query->setMulti($model->persistence->typecastSaveRow($model, $this->getAndUnsetSaveBuffer($entity))); $query->set($this->foreign_field, $this->hasJoin() ? $this->getJoin()->getId($entity) : $entity->getId()); $query->mode('insert')->execute(); // TODO IMPORTANT migrate to Model insert - $this->setId($entity, $model->persistence->lastInsertId($model)); + $modelForLastInsertId = $model; + while (is_object($modelForLastInsertId->table)) { + $modelForLastInsertId = $modelForLastInsertId->table; + } + // assumes same ID field across all nested models (not needed once migrated to Model insert) + $this->setId($entity, $model->persistence->lastInsertId($modelForLastInsertId)); } public function beforeUpdate(Model $entity, array &$data): void From 0b221eb59df7f234dc5b597ea2d2bae3e3d855ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 15 Jan 2022 00:34:40 +0100 Subject: [PATCH 03/16] fix TestCase::getDb() for "_id" ID --- src/Schema/TestCase.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Schema/TestCase.php b/src/Schema/TestCase.php index fab30ef3f..bfe854c7a 100644 --- a/src/Schema/TestCase.php +++ b/src/Schema/TestCase.php @@ -327,7 +327,12 @@ public function getDb(array $tableNames = null, bool $noId = false): array $s = $this->db->dsql(); $data = $s->table($table)->getRows(); + $idColumnName = null; foreach ($data as &$row) { + if ($idColumnName === null) { + $idColumnName = isset($row['_id']) ? '_id' : 'id'; + } + foreach ($row as &$val) { if (is_int($val)) { $val = (int) $val; @@ -335,10 +340,10 @@ public function getDb(array $tableNames = null, bool $noId = false): array } if ($noId) { - unset($row['id']); + unset($row[$idColumnName]); $data2[] = $row; } else { - $data2[$row['id']] = $row; + $data2[$row[$idColumnName]] = $row; } } From f8887c7d6ea05d6b70834c4dc5012f34750fa165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 15 Jan 2022 01:08:42 +0100 Subject: [PATCH 04/16] no iter by ref in TestCase::getDb() --- src/Schema/TestCase.php | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Schema/TestCase.php b/src/Schema/TestCase.php index bfe854c7a..3ea6f66f7 100644 --- a/src/Schema/TestCase.php +++ b/src/Schema/TestCase.php @@ -319,16 +319,14 @@ public function getDb(array $tableNames = null, bool $noId = false): array $tableNames = array_values($tableNames); } - $ret = []; - + $resAll = []; foreach ($tableNames as $table) { - $data2 = []; - - $s = $this->db->dsql(); - $data = $s->table($table)->getRows(); + $query = $this->db->dsql(); + $rows = $query->table($table)->getRows(); + $res = []; $idColumnName = null; - foreach ($data as &$row) { + foreach ($rows as $row) { if ($idColumnName === null) { $idColumnName = isset($row['_id']) ? '_id' : 'id'; } @@ -341,15 +339,15 @@ public function getDb(array $tableNames = null, bool $noId = false): array if ($noId) { unset($row[$idColumnName]); - $data2[] = $row; + $res[] = $row; } else { - $data2[$row[$idColumnName]] = $row; + $res[$row[$idColumnName]] = $row; } } - $ret[$table] = $data2; + $resAll[$table] = $res; } - return $ret; + return $resAll; } } From d8c7b80a85dcc56cf1f4e6f3c1344ef187f67b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 15 Jan 2022 14:11:36 +0100 Subject: [PATCH 05/16] rm redundant cast --- src/Persistence/Array_.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php index cda1734a7..691b71beb 100644 --- a/src/Persistence/Array_.php +++ b/src/Persistence/Array_.php @@ -374,8 +374,6 @@ protected function applyScope(Model $model, Action $action): void */ public function action(Model $model, string $type, array $args = []) { - $args = (array) $args; - switch ($type) { case 'select': $action = $this->initAction($model, $args[0] ?? null); From ce78c97b71674380d4387bee3f28aa716d03be3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 15 Jan 2022 15:28:17 +0100 Subject: [PATCH 06/16] fix phpstan v1.4.0 --- src/Persistence/Static_.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Persistence/Static_.php b/src/Persistence/Static_.php index 1ab963d96..8f4916fde 100644 --- a/src/Persistence/Static_.php +++ b/src/Persistence/Static_.php @@ -136,7 +136,7 @@ public function add(Model $model, array $defaults = []): void $hadData = true; if (!isset($this->data[$model->table])) { $hadData = false; - $this->data[$model->table] = true; // @phpstan-ignore-line + $this->data[$model->table] = true; } try { parent::add($model, $defaults); From 2eddc118307fcf1bb610eddd4837131f649018c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 15 Jan 2022 17:03:51 +0100 Subject: [PATCH 07/16] fix actual test for Array_ persistence --- src/Persistence/Array_.php | 19 ++++++++++++++++--- .../Array_/Action/RenameColumnIterator.php | 9 ++++++++- src/Persistence/Array_/Db/Table.php | 6 +++--- src/Schema/TestCase.php | 6 ------ tests/Persistence/ArrayTest.php | 5 ++++- 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php index 691b71beb..a422c7a16 100644 --- a/src/Persistence/Array_.php +++ b/src/Persistence/Array_.php @@ -185,9 +185,14 @@ public function add(Model $model, array $defaults = []): void $this->seedData($model); } - private function filterRowDataOnlyModelFields(Model $model, array $rowData): array + private function getPersistenceNameToNameMap(Model $model): array { - return array_intersect_key($rowData, array_map(fn (Field $f) => $f->short_name, $model->getFields())); + return array_flip(array_map(fn (Field $f) => $f->getPersistenceName(), $model->getFields())); + } + + private function filterRowDataOnlyModelFields(Model $model, array $rowDataRaw): array + { + return array_intersect_key($rowDataRaw, $this->getPersistenceNameToNameMap($model)); } public function tryLoad(Model $model, $id): ?array @@ -323,7 +328,15 @@ public function initAction(Model $model, array $fields = null): Action $rows = []; foreach ($table->getRows() as $row) { - $rows[$row->getValue($model->id_field)] = $this->filterRowDataOnlyModelFields($model, $row->getData()); + $rows[$row->getValue($model->getField($model->id_field)->getPersistenceName())] = $this->filterRowDataOnlyModelFields($model, $row->getData()); + } + + $map = $this->getPersistenceNameToNameMap($model); + foreach ($rows as $rowIndex => $row) { + $rows[$rowIndex] = []; + foreach ($row as $k => $v) { + $rows[$rowIndex][$map[$k]] = $v; + } } if ($fields !== null) { diff --git a/src/Persistence/Array_/Action/RenameColumnIterator.php b/src/Persistence/Array_/Action/RenameColumnIterator.php index d42ec2591..604e5be52 100644 --- a/src/Persistence/Array_/Action/RenameColumnIterator.php +++ b/src/Persistence/Array_/Action/RenameColumnIterator.php @@ -4,6 +4,8 @@ namespace Atk4\Data\Persistence\Array_\Action; +use Atk4\Data\Exception; + /** * @internal * @@ -32,7 +34,12 @@ public function current(): array $row = parent::current(); $keys = array_keys($row); - $keys[array_search($this->origName, $keys, true)] = $this->newName; + $index = array_search($this->origName, $keys, true); + if ($index === false) { + throw (new Exception('Column not found')) + ->addMoreInfo('orig_name', $this->origName); + } + $keys[$index] = $this->newName; return array_combine($keys, $row); } diff --git a/src/Persistence/Array_/Db/Table.php b/src/Persistence/Array_/Db/Table.php index 8bf0632d7..49107073c 100644 --- a/src/Persistence/Array_/Db/Table.php +++ b/src/Persistence/Array_/Db/Table.php @@ -181,12 +181,12 @@ protected function beforeValuesSet(Row $childRow, $newRowData): void /** * TODO rewrite with hash index support. * - * @param mixed $id + * @param mixed $idRaw */ - public function getRowById(\Atk4\Data\Model $model, $id): ?Row + public function getRowById(\Atk4\Data\Model $model, $idRaw): ?Row { foreach ($this->getRows() as $row) { - if ($row->getValue($model->id_field) === $id) { + if ($row->getValue($model->getField($model->id_field)->getPersistenceName()) === $idRaw) { return $row; } } diff --git a/src/Schema/TestCase.php b/src/Schema/TestCase.php index 3ea6f66f7..21ff2fca2 100644 --- a/src/Schema/TestCase.php +++ b/src/Schema/TestCase.php @@ -331,12 +331,6 @@ public function getDb(array $tableNames = null, bool $noId = false): array $idColumnName = isset($row['_id']) ? '_id' : 'id'; } - foreach ($row as &$val) { - if (is_int($val)) { - $val = (int) $val; - } - } - if ($noId) { unset($row[$idColumnName]); $res[] = $row; diff --git a/tests/Persistence/ArrayTest.php b/tests/Persistence/ArrayTest.php index 334506f5e..eeb336a08 100644 --- a/tests/Persistence/ArrayTest.php +++ b/tests/Persistence/ArrayTest.php @@ -583,7 +583,10 @@ public function testOrder(): void // order by one field descending $p = new Persistence\Array_($dbData); - $m = new Model($p); + $m = new Model($p, ['id_field' => 'myid']); + $m->id_field = 'id'; + $m->removeField('myid'); + $m->addField('id'); $m->getField('id')->actual = 'myid'; $m->addField('f1'); $m->addField('f2'); From 0a03a9698633d571ae060bd81dd11c63f0a9eee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 15 Jan 2022 22:53:42 +0100 Subject: [PATCH 08/16] add support for model nesting for Array_ persistence --- src/Persistence/Array_.php | 69 ++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php index a422c7a16..afa595d78 100644 --- a/src/Persistence/Array_.php +++ b/src/Persistence/Array_.php @@ -97,7 +97,9 @@ private function seedDataAndGetTable(Model $model): Table */ public function getRawDataByTable(Model $model, string $table): array { - $this->seedData($model); + if (!is_object($model->table)) { + $this->seedData($model); + } $rows = []; foreach ($this->data[$table]->getRows() as $row) { @@ -182,7 +184,9 @@ public function add(Model $model, array $defaults = []): void } } - $this->seedData($model); + if (!is_object($model->table)) { + $this->seedData($model); + } } private function getPersistenceNameToNameMap(Model $model): array @@ -195,10 +199,19 @@ private function filterRowDataOnlyModelFields(Model $model, array $rowDataRaw): return array_intersect_key($rowDataRaw, $this->getPersistenceNameToNameMap($model)); } - public function tryLoad(Model $model, $id): ?array + public function remapLoadRow(Model $model, array $row): array { - $table = $this->seedDataAndGetTable($model); + $rowRemapped = []; + $map = $this->getPersistenceNameToNameMap($model); + foreach ($row as $k => $v) { + $rowRemapped[$map[$k]] = $v; + } + return $rowRemapped; + } + + public function tryLoad(Model $model, $id): ?array + { if ($id === self::ID_LOAD_ONE || $id === self::ID_LOAD_ANY) { $action = $this->action($model, 'select'); $action->generator->rewind(); // TODO needed for some reasons! @@ -219,12 +232,29 @@ public function tryLoad(Model $model, $id): ?array return $row; } - $row = $table->getRowById($model, $id); - if ($row === null) { - return null; + if (is_object($model->table)) { + $action = $this->action($model, 'select'); + $condition = new Model\Scope\Condition('', $id); + $condition->key = $model->getField($model->id_field); + $action->filter($condition); + $action->generator->rewind(); // TODO needed for some reasons! + + $rowData = $action->getRow(); + if ($rowData === null) { + return null; + } + } else { + $table = $this->seedDataAndGetTable($model); + + $row = $table->getRowById($model, $id); + if ($row === null) { + return null; + } + + $rowData = $this->remapLoadRow($model, $this->filterRowDataOnlyModelFields($model, $row->getData())); } - return $this->typecastLoadRow($model, $this->filterRowDataOnlyModelFields($model, $row->getData())); + return $this->typecastLoadRow($model, $rowData); } protected function insertRaw(Model $model, array $dataRaw) @@ -324,19 +354,24 @@ public function export(Model $model, array $fields = null, bool $typecast = true */ public function initAction(Model $model, array $fields = null): Action { - $table = $this->seedDataAndGetTable($model); + if (is_object($model->table)) { + $tableAction = $this->action($model->table, 'select'); - $rows = []; - foreach ($table->getRows() as $row) { - $rows[$row->getValue($model->getField($model->id_field)->getPersistenceName())] = $this->filterRowDataOnlyModelFields($model, $row->getData()); + $rows = []; + foreach ($tableAction->getRows() as $k => $row) { + $rows[$k] = $this->filterRowDataOnlyModelFields($model, $row); + } + } else { + $table = $this->seedDataAndGetTable($model); + + $rows = []; + foreach ($table->getRows() as $row) { + $rows[$row->getValue($model->getField($model->id_field)->getPersistenceName())] = $this->filterRowDataOnlyModelFields($model, $row->getData()); + } } - $map = $this->getPersistenceNameToNameMap($model); foreach ($rows as $rowIndex => $row) { - $rows[$rowIndex] = []; - foreach ($row as $k => $v) { - $rows[$rowIndex][$map[$k]] = $v; - } + $rows[$rowIndex] = $this->remapLoadRow($model, $row); } if ($fields !== null) { From 8167d3e0633d48661764f143ad6ed787de6d3fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 15 Jan 2022 23:16:06 +0100 Subject: [PATCH 09/16] add model nesting test for Array_ persistence --- tests/ModelNestedArrayTest.php | 226 ++++++++++++++++++ ...lNestedTest.php => ModelNestedSqlTest.php} | 4 +- 2 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 tests/ModelNestedArrayTest.php rename tests/{ModelNestedTest.php => ModelNestedSqlTest.php} (99%) diff --git a/tests/ModelNestedArrayTest.php b/tests/ModelNestedArrayTest.php new file mode 100644 index 000000000..8450547a1 --- /dev/null +++ b/tests/ModelNestedArrayTest.php @@ -0,0 +1,226 @@ +db->connection->connection()->close(); + + $this->db = new Persistence\Array_([ + 'user' => [ + 1 => ['_id' => 1, 'name' => 'John', '_birthday' => '1980-02-01'], + ['_id' => 2, 'name' => 'Sue', '_birthday' => '2005-04-03'], + ['_id' => 3, 'name' => 'Veronica', '_birthday' => '2005-04-03'], + ], + ]); + } + + /** @var array */ + public $hookLog = []; + + protected function createTestModel(): Model + { + $mWithLoggingClass = get_class(new class() extends Model { + /** @var \WeakReference */ + protected $testCaseWeakRef; + /** @var string */ + protected $testModelAlias; + + /** + * @param mixed $v + * + * @return mixed + */ + protected function convertValueToLog($v) + { + if (is_array($v)) { + return array_map(fn ($v) => $this->convertValueToLog($v), $v); + } elseif (is_scalar($v) || $v === null) { + return $v; + } elseif ($v instanceof self) { + return $this->testModelAlias; + } + + return get_debug_type($v); + } + + public function hook(string $spot, array $args = [], HookBreaker &$brokenBy = null) + { + if (!str_starts_with($spot, '__atk__method__') && $spot !== Model::HOOK_NORMALIZE) { + $this->testCaseWeakRef->get()->hookLog[] = [$this->convertValueToLog($this), $spot, $this->convertValueToLog($args)]; + } + + return parent::hook($spot, $args, $brokenBy); + } + + public function atomic(\Closure $fx) + { + $this->testCaseWeakRef->get()->hookLog[] = [$this->convertValueToLog($this), '>>>']; + + $res = parent::atomic($fx); + + $this->testCaseWeakRef->get()->hookLog[] = [$this->convertValueToLog($this), '<<<']; + + return $res; + } + }); + + $mInner = new $mWithLoggingClass($this->db, [ + 'testCaseWeakRef' => \WeakReference::create($this), + 'testModelAlias' => 'inner', + 'table' => 'user', + 'id_field' => '_id', + ]); + $mInner->removeField('_id'); + $mInner->id_field = 'uid'; + $mInner->addField('uid', ['actual' => '_id', 'type' => 'integer']); + $mInner->addField('name'); + $mInner->addField('y', ['actual' => '_birthday', 'type' => 'date']); + $mInner->addCondition('uid', '!=', 3); + + $m = new $mWithLoggingClass($this->db, [ + 'testCaseWeakRef' => \WeakReference::create($this), + 'testModelAlias' => 'main', + 'table' => $mInner, + ]); + $m->removeField('id'); + $m->id_field = 'name'; + $m->addField('name'); + $m->addField('birthday', ['actual' => 'y', 'type' => 'date']); + + return $m; + } + + public function testSelectExport(): void + { + $m = $this->createTestModel(); + + $this->assertSameExportUnordered([ + 1 => ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + ], $m->export()); + + $this->assertSame([ + ], $this->hookLog); + } + + public function testInsert(): void + { + $m = $this->createTestModel(); + + $entity = $m->createEntity() + ->setMulti([ + 'name' => 'Karl', + 'birthday' => new \DateTime('2000-6-1'), + ])->save(); + + $this->assertSame([ + ['main', '>>>'], + ['main', Model::HOOK_VALIDATE, ['save']], + ['main', Model::HOOK_BEFORE_SAVE, [false]], + ['main', Model::HOOK_BEFORE_INSERT, [['name' => 'Karl', 'birthday' => \DateTime::class]]], + ['inner', '>>>'], + ['inner', Model::HOOK_VALIDATE, ['save']], + ['inner', Model::HOOK_BEFORE_SAVE, [false]], + ['inner', Model::HOOK_BEFORE_INSERT, [['uid' => null, 'name' => 'Karl', 'y' => \DateTime::class]]], + ['inner', Model::HOOK_AFTER_INSERT, []], + ['inner', Model::HOOK_AFTER_SAVE, [false]], + ['inner', '<<<'], + ['main', Model::HOOK_AFTER_INSERT, []], + ['main', Model::HOOK_BEFORE_UNLOAD, []], + ['main', Model::HOOK_AFTER_UNLOAD, []], + ['main', Model::HOOK_BEFORE_LOAD, ['Karl']], + ['main', Model::HOOK_AFTER_LOAD, []], + ['main', Model::HOOK_AFTER_SAVE, [false]], + ['main', '<<<'], + ], $this->hookLog); + + $this->assertSame(4, $m->table->loadBy('name', 'Karl')->getId()); + $this->assertSameExportUnordered([['Karl']], [[$entity->getId()]]); + + $this->assertSameExportUnordered([ + 1 => ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['name' => 'Sue', 'birthday' => new \DateTime('2005-4-3')], + 4 => ['name' => 'Karl', 'birthday' => new \DateTime('2000-6-1')], + ], $m->export()); + } + + public function testUpdate(): void + { + $m = $this->createTestModel(); + + $m->load('Sue') + ->setMulti([ + 'birthday' => new \DateTime('2020-7-8'), + ])->save(); + + $this->assertSame([ + ['main', Model::HOOK_BEFORE_LOAD, ['Sue']], + ['main', Model::HOOK_AFTER_LOAD, []], + + ['main', '>>>'], + ['main', Model::HOOK_VALIDATE, ['save']], + ['main', Model::HOOK_BEFORE_SAVE, [true]], + ['main', Model::HOOK_BEFORE_UPDATE, [['birthday' => \DateTime::class]]], + ['inner', Model::HOOK_BEFORE_LOAD, [null]], + ['inner', Model::HOOK_AFTER_LOAD, []], + ['inner', '>>>'], + ['inner', Model::HOOK_VALIDATE, ['save']], + ['inner', Model::HOOK_BEFORE_SAVE, [true]], + ['inner', Model::HOOK_BEFORE_UPDATE, [['y' => \DateTime::class]]], + ['inner', Model::HOOK_AFTER_UPDATE, [['y' => \DateTime::class]]], + ['inner', Model::HOOK_AFTER_SAVE, [true]], + ['inner', '<<<'], + ['main', Model::HOOK_AFTER_UPDATE, [['birthday' => \DateTime::class]]], + ['main', Model::HOOK_AFTER_SAVE, [true]], + ['main', '<<<'], + ], $this->hookLog); + + $this->assertSameExportUnordered([ + 1 => ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ['name' => 'Sue', 'birthday' => new \DateTime('2020-7-8')], + ], $m->export()); + } + + public function testDelete(): void + { + $m = $this->createTestModel(); + + $m->delete('Sue'); + + $this->assertSame([ + ['main', Model::HOOK_BEFORE_LOAD, ['Sue']], + ['main', Model::HOOK_AFTER_LOAD, []], + + ['main', '>>>'], + ['main', Model::HOOK_BEFORE_DELETE, []], + ['inner', Model::HOOK_BEFORE_LOAD, [null]], + ['inner', Model::HOOK_AFTER_LOAD, []], + ['inner', '>>>'], + ['inner', Model::HOOK_BEFORE_DELETE, []], + ['inner', Model::HOOK_AFTER_DELETE, []], + ['inner', '<<<'], + ['inner', Model::HOOK_BEFORE_UNLOAD, []], + ['inner', Model::HOOK_AFTER_UNLOAD, []], + ['main', Model::HOOK_AFTER_DELETE, []], + ['main', '<<<'], + ['main', Model::HOOK_BEFORE_UNLOAD, []], + ['main', Model::HOOK_AFTER_UNLOAD, []], + ], $this->hookLog); + + $this->assertSameExportUnordered([ + 1 => ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], + ], $m->export()); + } +} diff --git a/tests/ModelNestedTest.php b/tests/ModelNestedSqlTest.php similarity index 99% rename from tests/ModelNestedTest.php rename to tests/ModelNestedSqlTest.php index bb7b2ddf2..ef74ec61a 100644 --- a/tests/ModelNestedTest.php +++ b/tests/ModelNestedSqlTest.php @@ -12,7 +12,7 @@ use Atk4\Data\Schema\TestCase; use Doctrine\DBAL\Result as DbalResult; -class ModelNestedTest extends TestCase +class ModelNestedSqlTest extends TestCase { protected function setUp(): void { @@ -33,7 +33,7 @@ protected function setUp(): void protected function createTestModel(): Model { $mWithLoggingClass = get_class(new class() extends Model { - /** @var \WeakReference */ + /** @var \WeakReference */ protected $testCaseWeakRef; /** @var string */ protected $testModelAlias; From 79907fe67e03a88e8677f27f52f93535d37dd6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 15 Jan 2022 23:06:48 +0100 Subject: [PATCH 10/16] fix typecasting of object ID for Array_ persistence --- src/Persistence/Array_.php | 1 + tests/ModelNestedArrayTest.php | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php index afa595d78..ebae6c9fe 100644 --- a/src/Persistence/Array_.php +++ b/src/Persistence/Array_.php @@ -236,6 +236,7 @@ public function tryLoad(Model $model, $id): ?array $action = $this->action($model, 'select'); $condition = new Model\Scope\Condition('', $id); $condition->key = $model->getField($model->id_field); + $condition->setOwner($model->createEntity()); // TODO needed for typecasting to apply $action->filter($condition); $action->generator->rewind(); // TODO needed for some reasons! diff --git a/tests/ModelNestedArrayTest.php b/tests/ModelNestedArrayTest.php index 8450547a1..9d909e303 100644 --- a/tests/ModelNestedArrayTest.php +++ b/tests/ModelNestedArrayTest.php @@ -95,7 +95,7 @@ public function atomic(\Closure $fx) 'table' => $mInner, ]); $m->removeField('id'); - $m->id_field = 'name'; + $m->id_field = 'birthday'; $m->addField('name'); $m->addField('birthday', ['actual' => 'y', 'type' => 'date']); @@ -140,14 +140,14 @@ public function testInsert(): void ['main', Model::HOOK_AFTER_INSERT, []], ['main', Model::HOOK_BEFORE_UNLOAD, []], ['main', Model::HOOK_AFTER_UNLOAD, []], - ['main', Model::HOOK_BEFORE_LOAD, ['Karl']], + ['main', Model::HOOK_BEFORE_LOAD, [\DateTime::class]], ['main', Model::HOOK_AFTER_LOAD, []], ['main', Model::HOOK_AFTER_SAVE, [false]], ['main', '<<<'], ], $this->hookLog); $this->assertSame(4, $m->table->loadBy('name', 'Karl')->getId()); - $this->assertSameExportUnordered([['Karl']], [[$entity->getId()]]); + $this->assertSameExportUnordered([[new \DateTime('2000-6-1')]], [[$entity->getId()]]); $this->assertSameExportUnordered([ 1 => ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], @@ -160,36 +160,36 @@ public function testUpdate(): void { $m = $this->createTestModel(); - $m->load('Sue') + $m->load(new \DateTime('2005-4-3')) ->setMulti([ - 'birthday' => new \DateTime('2020-7-8'), + 'name' => 'Susan', ])->save(); $this->assertSame([ - ['main', Model::HOOK_BEFORE_LOAD, ['Sue']], + ['main', Model::HOOK_BEFORE_LOAD, [\DateTime::class]], ['main', Model::HOOK_AFTER_LOAD, []], ['main', '>>>'], ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [true]], - ['main', Model::HOOK_BEFORE_UPDATE, [['birthday' => \DateTime::class]]], + ['main', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan']]], ['inner', Model::HOOK_BEFORE_LOAD, [null]], ['inner', Model::HOOK_AFTER_LOAD, []], ['inner', '>>>'], ['inner', Model::HOOK_VALIDATE, ['save']], ['inner', Model::HOOK_BEFORE_SAVE, [true]], - ['inner', Model::HOOK_BEFORE_UPDATE, [['y' => \DateTime::class]]], - ['inner', Model::HOOK_AFTER_UPDATE, [['y' => \DateTime::class]]], + ['inner', Model::HOOK_BEFORE_UPDATE, [['name' => 'Susan']]], + ['inner', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan']]], ['inner', Model::HOOK_AFTER_SAVE, [true]], ['inner', '<<<'], - ['main', Model::HOOK_AFTER_UPDATE, [['birthday' => \DateTime::class]]], + ['main', Model::HOOK_AFTER_UPDATE, [['name' => 'Susan']]], ['main', Model::HOOK_AFTER_SAVE, [true]], ['main', '<<<'], ], $this->hookLog); $this->assertSameExportUnordered([ 1 => ['name' => 'John', 'birthday' => new \DateTime('1980-2-1')], - ['name' => 'Sue', 'birthday' => new \DateTime('2020-7-8')], + ['name' => 'Susan', 'birthday' => new \DateTime('2005-4-3')], ], $m->export()); } @@ -197,10 +197,10 @@ public function testDelete(): void { $m = $this->createTestModel(); - $m->delete('Sue'); + $m->delete(new \DateTime('2005-4-3')); $this->assertSame([ - ['main', Model::HOOK_BEFORE_LOAD, ['Sue']], + ['main', Model::HOOK_BEFORE_LOAD, [\DateTime::class]], ['main', Model::HOOK_AFTER_LOAD, []], ['main', '>>>'], From 3fa38de6c30cb38c81a5c17b3b46e74e4b22b4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 15 Jan 2022 23:53:49 +0100 Subject: [PATCH 11/16] fix visibility --- src/Persistence/Array_.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php index ebae6c9fe..fe7d55be6 100644 --- a/src/Persistence/Array_.php +++ b/src/Persistence/Array_.php @@ -199,7 +199,7 @@ private function filterRowDataOnlyModelFields(Model $model, array $rowDataRaw): return array_intersect_key($rowDataRaw, $this->getPersistenceNameToNameMap($model)); } - public function remapLoadRow(Model $model, array $row): array + private function remapLoadRow(Model $model, array $row): array { $rowRemapped = []; $map = $this->getPersistenceNameToNameMap($model); From 3308653283c53d44b4dd9530920a43eb067f8d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 15 Jan 2022 23:57:30 +0100 Subject: [PATCH 12/16] simplify array seed --- tests/ModelNestedArrayTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ModelNestedArrayTest.php b/tests/ModelNestedArrayTest.php index 9d909e303..0aea3a4a0 100644 --- a/tests/ModelNestedArrayTest.php +++ b/tests/ModelNestedArrayTest.php @@ -19,9 +19,9 @@ protected function setUp(): void $this->db = new Persistence\Array_([ 'user' => [ - 1 => ['_id' => 1, 'name' => 'John', '_birthday' => '1980-02-01'], - ['_id' => 2, 'name' => 'Sue', '_birthday' => '2005-04-03'], - ['_id' => 3, 'name' => 'Veronica', '_birthday' => '2005-04-03'], + 1 => ['name' => 'John', '_birthday' => '1980-02-01'], + ['name' => 'Sue', '_birthday' => '2005-04-03'], + ['name' => 'Veronica', '_birthday' => '2005-04-03'], ], ]); } From e7455c7769a62c2c20bcf105ac93c2ac0ce45279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 16 Jan 2022 00:01:57 +0100 Subject: [PATCH 13/16] dedup only fields filter --- src/Persistence/Array_.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php index fe7d55be6..697fe8f07 100644 --- a/src/Persistence/Array_.php +++ b/src/Persistence/Array_.php @@ -358,21 +358,18 @@ public function initAction(Model $model, array $fields = null): Action if (is_object($model->table)) { $tableAction = $this->action($model->table, 'select'); - $rows = []; - foreach ($tableAction->getRows() as $k => $row) { - $rows[$k] = $this->filterRowDataOnlyModelFields($model, $row); - } + $rows = $tableAction->getRows(); } else { $table = $this->seedDataAndGetTable($model); $rows = []; foreach ($table->getRows() as $row) { - $rows[$row->getValue($model->getField($model->id_field)->getPersistenceName())] = $this->filterRowDataOnlyModelFields($model, $row->getData()); + $rows[$row->getValue($model->getField($model->id_field)->getPersistenceName())] = $row->getData(); } } foreach ($rows as $rowIndex => $row) { - $rows[$rowIndex] = $this->remapLoadRow($model, $row); + $rows[$rowIndex] = $this->remapLoadRow($model, $this->filterRowDataOnlyModelFields($model, $row)); } if ($fields !== null) { From f531f295e33cdfbb6360d09c353162017debc055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 16 Jan 2022 00:14:44 +0100 Subject: [PATCH 14/16] fix stan for PHP 7.x --- src/Reference/HasOne.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Reference/HasOne.php b/src/Reference/HasOne.php index 6f0c97ec6..c4aa870d0 100644 --- a/src/Reference/HasOne.php +++ b/src/Reference/HasOne.php @@ -41,7 +41,7 @@ protected function init(): void $v = $this->{$fieldPropRefl->getName()}; $vDefault = \PHP_MAJOR_VERSION < 8 ? $fieldPropRefl->getDeclaringClass()->getDefaultProperties()[$fieldPropRefl->getName()] - : $fieldPropRefl->getDefaultValue(); + : (null ?? $fieldPropRefl->getDefaultValue()); // @phpstan-ignore-line for PHP 7.x if ($v !== $vDefault) { $fieldSeed[$fieldPropRefl->getName()] = $v; } From 9e6ef2486b77d5199cbf276e4a26296df9746bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 16 Jan 2022 00:18:37 +0100 Subject: [PATCH 15/16] fix cs --- tests/ModelNestedArrayTest.php | 6 +++--- tests/ModelNestedSqlTest.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/ModelNestedArrayTest.php b/tests/ModelNestedArrayTest.php index 0aea3a4a0..3bc4d0296 100644 --- a/tests/ModelNestedArrayTest.php +++ b/tests/ModelNestedArrayTest.php @@ -11,6 +11,9 @@ class ModelNestedArrayTest extends TestCase { + /** @var array */ + public $hookLog = []; + protected function setUp(): void { parent::setUp(); @@ -26,9 +29,6 @@ protected function setUp(): void ]); } - /** @var array */ - public $hookLog = []; - protected function createTestModel(): Model { $mWithLoggingClass = get_class(new class() extends Model { diff --git a/tests/ModelNestedSqlTest.php b/tests/ModelNestedSqlTest.php index ef74ec61a..f8e0a106c 100644 --- a/tests/ModelNestedSqlTest.php +++ b/tests/ModelNestedSqlTest.php @@ -14,6 +14,9 @@ class ModelNestedSqlTest extends TestCase { + /** @var array */ + public $hookLog = []; + protected function setUp(): void { parent::setUp(); @@ -27,9 +30,6 @@ protected function setUp(): void ]); } - /** @var array */ - public $hookLog = []; - protected function createTestModel(): Model { $mWithLoggingClass = get_class(new class() extends Model { From 9a2639217d6036d86dc9ac4c02a6e5b4d986b997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 16 Jan 2022 00:33:27 +0100 Subject: [PATCH 16/16] add no-change hooks assertion --- tests/ModelNestedArrayTest.php | 8 ++++++++ tests/ModelNestedSqlTest.php | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/tests/ModelNestedArrayTest.php b/tests/ModelNestedArrayTest.php index 3bc4d0296..301bc3283 100644 --- a/tests/ModelNestedArrayTest.php +++ b/tests/ModelNestedArrayTest.php @@ -161,6 +161,9 @@ public function testUpdate(): void $m = $this->createTestModel(); $m->load(new \DateTime('2005-4-3')) + ->setMulti([ + 'name' => 'Sue', // no change + ])->save() ->setMulti([ 'name' => 'Susan', ])->save(); @@ -169,6 +172,11 @@ public function testUpdate(): void ['main', Model::HOOK_BEFORE_LOAD, [\DateTime::class]], ['main', Model::HOOK_AFTER_LOAD, []], + ['main', '>>>'], + ['main', Model::HOOK_VALIDATE, ['save']], + ['main', Model::HOOK_BEFORE_SAVE, [true]], + ['main', '<<<'], + ['main', '>>>'], ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [true]], diff --git a/tests/ModelNestedSqlTest.php b/tests/ModelNestedSqlTest.php index f8e0a106c..3fb2f592f 100644 --- a/tests/ModelNestedSqlTest.php +++ b/tests/ModelNestedSqlTest.php @@ -205,6 +205,9 @@ public function testUpdate(): void $m = $this->createTestModel(); $m->load(new \DateTime('2005-4-3')) + ->setMulti([ + 'name' => 'Sue', // no change + ])->save() ->setMulti([ 'name' => 'Susan', ])->save(); @@ -215,6 +218,11 @@ public function testUpdate(): void ['main', Persistence\Sql::HOOK_INIT_SELECT_QUERY, [Query::class, 'select']], ['main', Model::HOOK_AFTER_LOAD, []], + ['main', '>>>'], + ['main', Model::HOOK_VALIDATE, ['save']], + ['main', Model::HOOK_BEFORE_SAVE, [true]], + ['main', '<<<'], + ['main', '>>>'], ['main', Model::HOOK_VALIDATE, ['save']], ['main', Model::HOOK_BEFORE_SAVE, [true]],