Skip to content

Commit

Permalink
Add support for Model nesting (#946)
Browse files Browse the repository at this point in the history
Sponsored by @mkrecek234, thanks!
  • Loading branch information
mvorisek committed Jan 12, 2022
1 parent 72aadc0 commit e91bf15
Show file tree
Hide file tree
Showing 14 changed files with 514 additions and 182 deletions.
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

0 comments on commit e91bf15

Please sign in to comment.