Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for Model nesting #946

Merged
merged 17 commits into from
Jan 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,12 @@
"homepage": "https://github.com/atk4/data",
"require": {
"php": ">=7.4 <8.2",
"ext-intl": "*",
"ext-pdo": "*",
"atk4/core": "dev-develop",
"doctrine/dbal": "^2.13.5 || ^3.2",
"mvorisek/atk4-hintable": "~1.7.1"
},
"require-release": {
"php": ">=7.4 <8.2",
"ext-intl": "*",
"ext-pdo": "*",
"atk4/core": "~3.2.0",
"doctrine/dbal": "^2.13.5 || ^3.2",
"mvorisek/atk4-hintable": "~1.7.1"
Expand Down
3 changes: 0 additions & 3 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,8 @@ parameters:
# for src/Field/SqlExpressionField.php
- '~^Call to an undefined method Atk4\\Data\\Model::expr\(\)\.$~'
# for src/Model.php
- '~^Call to an undefined method Atk4\\Data\\Persistence::update\(\)\.$~'
- '~^Call to an undefined method Atk4\\Data\\Persistence::insert\(\)\.$~'
- '~^Call to an undefined method Atk4\\Data\\Persistence::export\(\)\.$~'
- '~^Call to an undefined method Atk4\\Data\\Persistence::prepareIterator\(\)\.$~'
- '~^Call to an undefined method Atk4\\Data\\Persistence::delete\(\)\.$~'
- '~^Call to an undefined method Atk4\\Data\\Persistence::action\(\)\.$~'
# for src/Model/ReferencesTrait.php (in context of class Atk4\Data\Model)
- '~^Call to an undefined method Atk4\\Data\\Reference::refLink\(\)\.$~'
Expand Down
2 changes: 1 addition & 1 deletion src/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ public function __debugInfo(): array
];

foreach ([
'system', 'never_persist', 'never_save', 'read_only', 'ui', 'joinName',
'actual', 'system', 'never_persist', 'never_save', 'read_only', 'ui', 'joinName',
] as $key) {
if ($this->{$key} !== null) {
$arr[$key] = $this->{$key};
Expand Down
25 changes: 5 additions & 20 deletions src/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class Model implements \IteratorAggregate
* model normally lives. The interpretation of the table will be decoded
* by persistence driver.
*
* @var string|false
* @var string|self|false
*/
public $table;

Expand Down Expand Up @@ -1612,14 +1612,8 @@ public function save(array $data = [])
});
}

/**
* This is a temporary method to avoid code duplication, but insert / import should
* be implemented differently.
*/
protected function _rawInsert(array $row): void
protected function _insert(array $row): void
{
$this->unload();

// Find any row values that do not correspond to fields, and they may correspond to
// references instead
$refs = [];
Expand Down Expand Up @@ -1660,26 +1654,17 @@ protected function _rawInsert(array $row): void
}

/**
* Faster method to add data, that does not modify active record.
*
* Will be further optimized in the future.
*
* @return mixed
*/
public function insert(array $row)
{
$model = $this->createEntity();
$model->_rawInsert($row);
$entity = $this->createEntity();
$entity->_insert($row);

return $this->id_field ? $model->getId() : null;
return $this->id_field ? $entity->getId() : null;
}

/**
* Even more faster method to add data, does not modify your
* current record and will not return anything.
*
* Will be further optimized in the future.
*
* @return $this
*/
public function import(array $rows)
Expand Down
16 changes: 13 additions & 3 deletions src/Model/Join.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ static function (Model $entity) use ($name): self {
);
}

private function getModelTableString(Model $model): string
{
if (is_object($model->table)) {
return $this->getModelTableString($model->table);
}

return $model->table;
}

/**
* Will use either foreign_alias or create #join_<table>.
*/
Expand All @@ -204,15 +213,16 @@ protected function init(): void
if ($this->reverse === true) {
if ($this->master_field && $this->master_field !== $id_field) { // TODO not implemented yet, see https://github.com/atk4/data/issues/803
throw (new Exception('Joining tables on non-id fields is not implemented yet'))
->addMoreInfo('condition', $this->getOwner()->table . '.' . $this->master_field . ' = ' . $this->foreign_table . '.' . $this->foreign_field);
->addMoreInfo('master_field', $this->master_field)
->addMoreInfo('id_field', $this->id_field);
}

if (!$this->master_field) {
$this->master_field = $id_field;
}

if (!$this->foreign_field) {
$this->foreign_field = $this->getOwner()->table . '_' . $id_field;
$this->foreign_field = $this->getModelTableString($this->getOwner()) . '_' . $id_field;
}
} else {
$this->reverse = false;
Expand Down Expand Up @@ -310,7 +320,7 @@ public function hasMany(string $link, array $defaults = [])
{
$defaults = array_merge([
'our_field' => $this->id_field,
'their_field' => $this->getOwner()->table . '_' . $this->id_field,
'their_field' => $this->getModelTableString($this->getOwner()) . '_' . $this->id_field,
], $defaults);

return $this->getOwner()->hasMany($link, $defaults);
Expand Down
124 changes: 124 additions & 0 deletions src/Persistence.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,130 @@ public function load(Model $model, $id): array
return $data;
}

/**
* Inserts record in database and returns new record ID.
*
* @return mixed
*/
public function insert(Model $model, array $data)
{
if ($model->id_field && array_key_exists($model->id_field, $data) && $data[$model->id_field] === null) {
unset($data[$model->id_field]);
}

$dataRaw = $this->typecastSaveRow($model, $data);
unset($data);

if (is_object($model->table)) {
$innerInsertId = $model->table->insert($this->typecastLoadRow($model->table, $dataRaw));
if (!$model->id_field) {
return false;
}

$idField = $model->getField($model->id_field);
$insertId = $this->typecastLoadField(
$idField,
$idField->getPersistenceName() === $model->table->id_field
? $this->typecastSaveField($model->table->getField($model->table->id_field), $innerInsertId)
: $dataRaw[$idField->getPersistenceName()]
);

return $insertId;
}

$idRaw = $this->insertRaw($model, $dataRaw);
if (!$model->id_field) {
return false;
}

$id = $this->typecastLoadField($model->getField($model->id_field), $idRaw);

return $id;
}

/**
* @return mixed
*/
protected function insertRaw(Model $model, array $dataRaw)
{
throw new Exception('Insert is not supported.');
}

/**
* Updates record in database.
*
* @param mixed $id
*/
public function update(Model $model, $id, array $data): void
{
$idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null;
unset($id);
if ($idRaw === null || (array_key_exists($model->id_field, $data) && $data[$model->id_field] === null)) {
throw new Exception('Model id_field is not set. Unable to update record.');
}

$dataRaw = $this->typecastSaveRow($model, $data);
unset($data);

if (count($dataRaw) === 0) {
return;
}

if (is_object($model->table)) {
$idPersistenceName = $model->getField($model->id_field)->getPersistenceName();
$innerId = $this->typecastLoadField($model->table->getField($idPersistenceName), $idRaw);
$innerModel = $model->table->loadBy($idPersistenceName, $innerId);

$innerModel->save($this->typecastLoadRow($model->table, $dataRaw));

return;
}

$this->updateRaw($model, $idRaw, $dataRaw);
}

/**
* @param mixed $idRaw
*/
protected function updateRaw(Model $model, $idRaw, array $dataRaw): void
{
throw new Exception('Update is not supported.');
}

/**
* Deletes record from database.
*
* @param mixed $id
*/
public function delete(Model $model, $id): void
{
$idRaw = $model->id_field ? $this->typecastSaveField($model->getField($model->id_field), $id) : null;
unset($id);
if ($idRaw === null) {
throw new Exception('Model id_field is not set. Unable to delete record.');
}

if (is_object($model->table)) {
$idPersistenceName = $model->getField($model->id_field)->getPersistenceName();
$innerId = $this->typecastLoadField($model->table->getField($idPersistenceName), $idRaw);
$innerModel = $model->table->loadBy($idPersistenceName, $innerId);

$innerModel->delete();

return;
}

$this->deleteRaw($model, $idRaw);
}

/**
* @param mixed $idRaw
*/
protected function deleteRaw(Model $model, $idRaw): void
{
throw new Exception('Delete is not supported.');
}

/**
* Will convert one row of data from native PHP types into
* persistence types. This will also take care of the "actual"
Expand Down
38 changes: 8 additions & 30 deletions src/Persistence/Array_.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,51 +222,29 @@ public function tryLoad(Model $model, $id): ?array
return $this->typecastLoadRow($model, $this->filterRowDataOnlyModelFields($model, $row->getData()));
}

/**
* Inserts record in data array and returns new record ID.
*
* @return mixed
*/
public function insert(Model $model, array $data)
protected function insertRaw(Model $model, array $dataRaw)
{
$this->seedData($model);

if ($model->id_field && ($data[$model->id_field] ?? null) === null) {
unset($data[$model->id_field]);
}
$data = $this->typecastSaveRow($model, $data);

$id = $data[$model->id_field] ?? $this->generateNewId($model);
$idRaw = $dataRaw[$model->id_field] ?? $this->generateNewId($model);

$this->saveRow($model, $data, $id);
$this->saveRow($model, $dataRaw, $idRaw);

return $id;
return $idRaw;
}

/**
* Updates record in data array and returns record ID.
*
* @param mixed $id
*/
public function update(Model $model, $id, array $data): void
protected function updateRaw(Model $model, $idRaw, array $dataRaw): void
{
$table = $this->seedDataAndGetTable($model);

$data = $this->typecastSaveRow($model, $data);

$this->saveRow($model, array_merge($this->filterRowDataOnlyModelFields($model, $table->getRowById($model, $id)->getData()), $data), $id);
$this->saveRow($model, array_merge($this->filterRowDataOnlyModelFields($model, $table->getRowById($model, $idRaw)->getData()), $dataRaw), $idRaw);
}

/**
* Deletes record in data array.
*
* @param mixed $id
*/
public function delete(Model $model, $id): void
protected function deleteRaw(Model $model, $idRaw): void
{
$table = $this->seedDataAndGetTable($model);

$table->deleteRow($table->getRowById($model, $id));
$table->deleteRow($table->getRowById($model, $idRaw));
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Persistence/Array_/Join.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ public function beforeUpdate(Model $entity, array &$data): void

$persistence = $this->persistence ?: $this->getOwner()->persistence;

// @phpstan-ignore-next-line TODO this cannot work, Persistence::update() returns void
$this->setId($entity, $persistence->update(
$this->makeFakeModelWithForeignTable(),
$this->getId($entity),
$this->getAndUnsetSaveBuffer($entity),
$this->foreign_table
$this->getAndUnsetSaveBuffer($entity)
));
}

Expand Down
Loading