diff --git a/docs/joins.rst b/docs/joins.rst
index 5e6f8a9ee2..a4f418618e 100644
--- a/docs/joins.rst
+++ b/docs/joins.rst
@@ -1,7 +1,11 @@
-================================
-Model from multiple joined table
-================================
+.. _Joins:
+
+.. php:namespace:: atk4\data\Model
+
+=================================
+Model from multiple joined tables
+=================================
.. php:class:: Join
diff --git a/src/Join.php b/src/Join.php
index 62f636c84a..dd55065176 100644
--- a/src/Join.php
+++ b/src/Join.php
@@ -4,464 +4,13 @@
namespace atk4\data;
-use atk4\core\DiContainerTrait;
-use atk4\core\InitializerTrait;
-use atk4\core\TrackableTrait;
+if (!class_exists(\SebastianBergmann\CodeCoverage\CodeCoverage::class, false)) {
+ 'trigger_error'('Class atk4\data\Join is deprecated. Use atk4\data\Model\Join instead', E_USER_DEPRECATED);
+}
/**
- * Class description?
- *
- * @property Model $owner
+ * @deprecated use \atk4\data\Model\Join instead - will be removed in dec-2020
*/
-class Join
+class Join extends Model\Join
{
- use TrackableTrait;
- use InitializerTrait {
- init as _init;
- }
- use DiContainerTrait;
-
- /**
- * Name of the table (or collection) that can be used to retrieve data from.
- * For SQL, This can also be an expression or sub-select.
- *
- * @var string
- */
- protected $foreign_table;
-
- /**
- * If $persistence is set, then it's used for loading
- * and storing the values, instead $owner->persistence.
- *
- * @var Persistence
- */
- protected $persistence;
-
- /**
- * ID used by a joined table.
- *
- * @var mixed
- */
- protected $id;
-
- /**
- * Field that is used as native "ID" in the foreign table.
- * When deleting record, this field will be conditioned.
- *
- * ->where($join->id_field, $join->id)->delete();
- *
- * @var string
- */
- protected $id_field = 'id';
-
- /**
- * By default this will be either "inner" (for strong) or "left" for weak joins.
- * You can specify your own type of join by passing ['kind'=>'right']
- * as second argument to join().
- *
- * @var string
- */
- protected $kind;
-
- /**
- * Is our join weak? Weak join will stop you from touching foreign table.
- *
- * @var bool
- */
- protected $weak = false;
-
- /**
- * Normally the foreign table is saved first, then it's ID is used in the
- * primary table. When deleting, the primary table record is deleted first
- * which is followed by the foreign table record.
- *
- * If you are using the following syntax:
- *
- * $user->join('contact','default_contact_id');
- *
- * Then the ID connecting tables is stored in foreign table and the order
- * of saving and delete needs to be reversed. In this case $reverse
- * will be set to `true`. You can specify value of this property.
- *
- * @var bool
- */
- protected $reverse;
-
- /**
- * Field to be used for matching inside master field. By default
- * it's $foreign_table.'_id'.
- *
- * @var string
- */
- protected $master_field;
-
- /**
- * Field to be used for matching in a foreign table. By default
- * it's 'id'.
- *
- * @var string
- */
- protected $foreign_field;
-
- /**
- * A short symbolic name that will be used as an alias for the joined table.
- *
- * @var string
- */
- public $foreign_alias;
-
- /**
- * When $prefix is set, then all the fields generated through
- * our wrappers will be automatically prefixed inside the model.
- *
- * @var string
- */
- protected $prefix = '';
-
- /**
- * Data which is populated here as the save/insert progresses.
- *
- * @var array
- */
- protected $save_buffer = [];
-
- /**
- * When join is done on another join.
- *
- * @var Join
- */
- protected $join;
-
- /**
- * Default constructor. Will copy argument into properties.
- *
- * @param array $defaults
- */
- public function __construct($foreign_table = null)
- {
- if (isset($foreign_table)) {
- $this->foreign_table = $foreign_table;
- }
- }
-
- /**
- * Will use either foreign_alias or create #join_
.
- */
- public function getDesiredName(): string
- {
- return '#join_' . $this->foreign_table;
- }
-
- /**
- * Initialization.
- */
- public function init(): void
- {
- $this->_init();
-
- // handle foreign table containing a dot
- if (is_string($this->foreign_table) && strpos($this->foreign_table, '.') !== false) {
- if (!isset($this->reverse)) {
- $this->reverse = true;
- if (isset($this->master_field)) {
- // both master and foreign fields are set
-
- // master_field exists, no we will use that
- // if (!is_object($this->master_field) && !$this->owner->hasField($this->master_field)) {
- throw (new Exception('You are trying to link tables on non-id fields. This is not implemented yet'))
- ->addMoreInfo('condition', $this->owner->table . '.' . $this->master_field . ' = ' . $this->foreign_table);
- // } $this->reverse = 'link';
- }
- }
-
- // split by LAST dot in foreign_table name
- [$this->foreign_table, $this->foreign_field] = preg_split('/\.+(?=[^\.]+$)/', $this->foreign_table);
-
- if (!$this->master_field) {
- $this->master_field = 'id';
- }
- } else {
- $this->reverse = false;
- $id_field = $this->owner->id_field ?: 'id';
- if (!$this->master_field) {
- $this->master_field = $this->foreign_table . '_' . $id_field;
- }
-
- if (!$this->foreign_field) {
- $this->foreign_field = $id_field;
- }
- }
-
- $this->owner->onHook(Model::HOOK_AFTER_UNLOAD, \Closure::fromCallable([$this, 'afterUnload']));
- }
-
- /**
- * Adding field into join will automatically associate that field
- * with this join. That means it won't be loaded from $table, but
- * form the join instead.
- *
- * @param string $name
- * @param array $seed
- *
- * @return Field
- */
- public function addField($name, $seed = [])
- {
- if ($seed && !is_array($seed)) {
- $seed = [$seed];
- }
- $seed['join'] = $this;
-
- return $this->owner->addField($this->prefix . $name, $seed);
- }
-
- /**
- * Adds multiple fields.
- *
- * @param array $fields
- *
- * @return $this
- */
- public function addFields($fields = [])
- {
- foreach ($fields as $field) {
- if (is_array($field)) {
- $name = $field[0];
- unset($field[0]);
- $this->addField($name, $field);
- } else {
- $this->addField($field);
- }
- }
-
- return $this;
- }
-
- /**
- * Adds any object to owner model.
- */
- public function add(object $object, array $defaults = []): object
- {
- if (!is_array($defaults)) {
- $defaults = ['name' => $defaults];
- }
-
- $defaults['join'] = $this;
-
- return $this->owner->add($object, $defaults);
- }
-
- /**
- * Another join will be attached to a current join.
- *
- * @param array $defaults
- *
- * @return Join
- */
- public function join(string $foreign_table, $defaults = [])
- {
- if (!is_array($defaults)) {
- $defaults = ['master_field' => $defaults];
- }
- $defaults['join'] = $this;
-
- return $this->owner->join($foreign_table, $defaults);
- }
-
- /**
- * Another leftJoin will be attached to a current join.
- *
- * @param array $defaults
- *
- * @return Join
- */
- public function leftJoin(string $foreign_table, $defaults = [])
- {
- if (!is_array($defaults)) {
- $defaults = ['master_field' => $defaults];
- }
- $defaults['join'] = $this;
-
- return $this->owner->leftJoin($foreign_table, $defaults);
- }
-
- /**
- * weakJoin will be attached to a current join.
- *
- * @todo NOT IMPLEMENTED! weakJoin method does not exist!
- *
- * @param array $defaults
- *
- * @return
- */
- /*
- public function weakJoin($defaults = [])
- {
- $defaults['join'] = $this;
-
- return $this->owner->weakJoin($defaults);
- }
- */
-
- /**
- * Creates reference based on a field from the join.
- *
- * @param string $link
- * @param array $defaults
- *
- * @return Reference\HasOne
- */
- public function hasOne($link, $defaults = [])
- {
- if (!is_array($defaults)) {
- $defaults = ['model' => $defaults ?: 'Model_' . $link];
- }
-
- $defaults['join'] = $this;
-
- return $this->owner->hasOne($link, $defaults);
- }
-
- /**
- * Creates reference based on the field from the join.
- *
- * @param string $link
- * @param array $defaults
- *
- * @return Reference\HasMany
- */
- public function hasMany($link, $defaults = [])
- {
- if (!is_array($defaults)) {
- $defaults = ['model' => $defaults ?: 'Model_' . $link];
- }
-
- $defaults = array_merge([
- 'our_field' => $this->id_field,
- 'their_field' => $this->owner->table . '_' . $this->id_field,
- ], $defaults);
-
- return $this->owner->hasMany($link, $defaults);
- }
-
- /**
- * Wrapper for containsOne that will associate field
- * with join.
- *
- * @todo NOT IMPLEMENTED !
- *
- * @param Model $model
- * @param array $defaults
- *
- * @return ???
- */
- /*
- public function containsOne($model, $defaults = [])
- {
- if (!is_array($defaults)) {
- $defaults = [$defaults];
- }
-
- if (is_string($defaults[0])) {
- $defaults[0] = $this->addField($defaults[0], ['system' => true]);
- }
-
- return parent::containsOne($model, $defaults);
- }
- */
-
- /**
- * Wrapper for containsMany that will associate field
- * with join.
- *
- * @todo NOT IMPLEMENTED !
- *
- * @param Model $model
- * @param array $defaults
- *
- * @return ???
- */
- /*
- public function containsMany($model, $defaults = [])
- {
- if (!is_array($defaults)) {
- $defaults = [$defaults];
- }
-
- if (is_string($defaults[0])) {
- $defaults[0] = $this->addField($defaults[0], ['system' => true]);
- }
-
- return parent::containsMany($model, $defaults);
- }
- */
-
- /**
- * Will iterate through this model by pulling
- * - fields
- * - references
- * - conditions.
- *
- * and then will apply them locally. If you think that any fields
- * could clash, then use ['prefix'=>'m2'] which will be pre-pended
- * to all the fields. Conditions will be automatically mapped.
- *
- * @todo NOT IMPLEMENTED !
- *
- * @param Model $model
- * @param array $defaults
- */
- /*
- public function importModel($model, $defaults = [])
- {
- // not implemented yet !!!
- }
- */
-
- /**
- * Joins with the primary table of the model and
- * then import all of the data into our model.
- *
- * @todo NOT IMPLEMENTED!
- *
- * @param Model $model
- * @param array $fields
- */
- /*
- public function weakJoinModel($model, $fields = [])
- {
- if (!is_object($model)) {
- $model = $this->owner->connection->add($model);
- }
- $j = $this->join($model->table);
-
- $j->importModel($model);
-
- return $j;
- }
- */
-
- /**
- * Set value.
- *
- * @param string $field
- * @param mixed $value
- *
- * @return $this
- */
- public function set($field, $value)
- {
- $this->save_buffer[$field] = $value;
-
- return $this;
- }
-
- /**
- * Clears id and save buffer.
- */
- protected function afterUnload()
- {
- $this->id = null;
- $this->save_buffer = [];
- }
}
diff --git a/src/Join/Array_.php b/src/Join/Array_.php
index b3a5cc1b76..1f162d0371 100644
--- a/src/Join/Array_.php
+++ b/src/Join/Array_.php
@@ -4,161 +4,13 @@
namespace atk4\data\Join;
-use atk4\data\Exception;
-use atk4\data\Join;
-use atk4\data\Model;
+if (!class_exists(\SebastianBergmann\CodeCoverage\CodeCoverage::class, false)) {
+ 'trigger_error'('Class atk4\data\Join\Array_ is deprecated. Use atk4\data\Persistence\Array_\Join instead', E_USER_DEPRECATED);
+}
/**
- * Join\Array_ class.
+ * @deprecated use \atk4\data\Persistence\Array_\Join instead - will be removed in dec-2020
*/
-class Array_ extends Join
+class Array_ extends \atk4\data\Persistence\Array_\Join
{
- /**
- * This method is to figure out stuff.
- */
- public function init(): void
- {
- parent::init();
-
- // If kind is not specified, figure out join type
- if (!isset($this->kind)) {
- $this->kind = $this->weak ? 'left' : 'inner';
- }
-
- // Add necessary hooks
- if ($this->reverse) {
- $this->owner->onHook(Model::HOOK_AFTER_INSERT, \Closure::fromCallable([$this, 'afterInsert']), [], -5);
- $this->owner->onHook(Model::HOOK_BEFORE_UPDATE, \Closure::fromCallable([$this, 'beforeUpdate']), [], -5);
- $this->owner->onHook(Model::HOOK_BEFORE_DELETE, \Closure::fromCallable([$this, 'doDelete']), [], -5);
- } else {
- $this->owner->onHook(Model::HOOK_BEFORE_INSERT, \Closure::fromCallable([$this, 'beforeInsert']));
- $this->owner->onHook(Model::HOOK_BEFORE_UPDATE, \Closure::fromCallable([$this, 'beforeUpdate']));
- $this->owner->onHook(Model::HOOK_AFTER_DELETE, \Closure::fromCallable([$this, 'doDelete']));
- $this->owner->onHook(Model::HOOK_AFTER_LOAD, \Closure::fromCallable([$this, 'afterLoad']));
- }
- }
-
- /**
- * Called from afterLoad hook.
- *
- * @param Model $model
- */
- public function afterLoad($model)
- {
- // we need to collect ID
- $this->id = $model->data[$this->master_field];
- if (!$this->id) {
- return;
- }
-
- try {
- $data = $model->persistence->load($model, $this->id, $this->foreign_table);
- } catch (Exception $e) {
- throw (new Exception('Unable to load joined record', $e->getCode(), $e))
- ->addMoreInfo('table', $this->foreign_table)
- ->addMoreInfo('id', $this->id);
- }
- $model->data = array_merge($data, $model->data);
- }
-
- /**
- * Called from beforeInsert hook.
- *
- * @param Model $model
- * @param array $data
- */
- public function beforeInsert($model, &$data)
- {
- if ($this->weak) {
- return;
- }
-
- if ($model->hasField($this->master_field) && $model->get($this->master_field)) {
- // The value for the master_field is set,
- // we are going to use existing record.
- return;
- }
-
- // Figure out where are we going to save data
- $persistence = $this->persistence ?:
- $this->owner->persistence;
-
- $this->id = $persistence->insert(
- $model,
- $this->save_buffer,
- $this->foreign_table
- );
-
- $data[$this->master_field] = $this->id;
-
- //$this->owner->set($this->master_field, $this->id);
- }
-
- /**
- * Called from afterInsert hook.
- *
- * @param Model $model
- * @param mixed $id
- */
- public function afterInsert($model, $id)
- {
- if ($this->weak) {
- return;
- }
-
- $this->save_buffer[$this->foreign_field] = isset($this->join) ? $this->join->id : $id;
-
- $persistence = $this->persistence ?: $this->owner->persistence;
-
- $this->id = $persistence->insert(
- $model,
- $this->save_buffer,
- $this->foreign_table
- );
- }
-
- /**
- * Called from beforeUpdate hook.
- *
- * @param Model $model
- * @param array $data
- */
- public function beforeUpdate($model, &$data)
- {
- if ($this->weak) {
- return;
- }
-
- $persistence = $this->persistence ?: $this->owner->persistence;
-
- $this->id = $persistence->update(
- $model,
- $this->id,
- $this->save_buffer,
- $this->foreign_table
- );
- }
-
- /**
- * Called from beforeDelete and afterDelete hooks.
- *
- * @param Model $model
- * @param mixed $id
- */
- public function doDelete($model, $id)
- {
- if ($this->weak) {
- return;
- }
-
- $persistence = $this->persistence ?: $this->owner->persistence;
-
- $persistence->delete(
- $model,
- $this->id,
- $this->foreign_table
- );
-
- $this->id = null;
- }
}
diff --git a/src/Join/Sql.php b/src/Join/Sql.php
index f118cbca67..9da9d04d25 100644
--- a/src/Join/Sql.php
+++ b/src/Join/Sql.php
@@ -4,284 +4,13 @@
namespace atk4\data\Join;
-use atk4\data\Join;
-use atk4\data\Model;
-use atk4\data\Persistence;
+if (!class_exists(\SebastianBergmann\CodeCoverage\CodeCoverage::class, false)) {
+ 'trigger_error'('Class atk4\data\Join\Sql is deprecated. Use atk4\data\Persistence\Sql\Join instead', E_USER_DEPRECATED);
+}
/**
- * Join\Sql class.
- *
- * @property Persistence\Sql $persistence
- * @property Sql $join
+ * @deprecated use \atk4\data\Persistence\Sql\Join instead - will be removed in dec-2020
*/
-class Sql extends Join implements \atk4\dsql\Expressionable
+class Sql extends \atk4\data\Persistence\Sql\Join
{
- /**
- * By default we create ON expression ourselves, but if you want to specify
- * it, use the 'on' property.
- *
- * @var \atk4\dsql\Expression
- */
- protected $on;
-
- /**
- * Will use either foreign_alias or create #join_.
- */
- public function getDesiredName(): string
- {
- return '_' . ($this->foreign_alias ?: $this->foreign_table[0]);
- }
-
- /**
- * Returns DSQL Expression.
- *
- * @param \atk4\dsql\Expression $q
- *
- * @return \atk4\dsql\Expression
- */
- public function getDsqlExpression($q)
- {
- /*
- // If our Model has expr() method (inherited from Persistence\Sql) then use it
- if ($this->owner->hasMethod('expr')) {
- return $this->owner->expr('{}.{}', [$this->foreign_alias, $this->foreign_field]);
- }
-
- // Otherwise call it from expression itself
- return $q->expr('{}.{}', [$this->foreign_alias, $this->foreign_field]);
- */
-
- // Romans: Join\Sql shouldn't even be called if expr is undefined. I think we should leave it here to produce error.
- return $this->owner->expr('{}.{}', [$this->foreign_alias, $this->foreign_field]);
- }
-
- /**
- * This method is to figure out stuff.
- */
- public function init(): void
- {
- parent::init();
-
- $this->owner->persistence_data['use_table_prefixes'] = true;
-
- // If kind is not specified, figure out join type
- if (!isset($this->kind)) {
- $this->kind = $this->weak ? 'left' : 'inner';
- }
-
- // Our short name will be unique
- if (!$this->foreign_alias) {
- $this->foreign_alias = ($this->owner->table_alias ?: '') . $this->short_name;
- }
-
- $this->owner->onHook(Persistence\Sql::HOOK_INIT_SELECT_QUERY, \Closure::fromCallable([$this, 'initSelectQuery']));
-
- // Add necessary hooks
- if ($this->reverse) {
- $this->owner->onHook(Model::HOOK_AFTER_INSERT, \Closure::fromCallable([$this, 'afterInsert']));
- $this->owner->onHook(Model::HOOK_BEFORE_UPDATE, \Closure::fromCallable([$this, 'beforeUpdate']));
- $this->owner->onHook(Model::HOOK_BEFORE_DELETE, \Closure::fromCallable([$this, 'doDelete']), [], -5);
- $this->owner->onHook(Model::HOOK_AFTER_LOAD, \Closure::fromCallable([$this, 'afterLoad']));
- } else {
- // Master field indicates ID of the joined item. In the past it had to be
- // defined as a physical field in the main table. Now it is a model field
- // so you can use expressions or fields inside joined entities.
- // If string specified here does not point to an existing model field
- // a new basic field is inserted and marked hidden.
- if (is_string($this->master_field)) {
- if (!$this->owner->hasField($this->master_field)) {
- if ($this->join) {
- $f = $this->join->addField($this->master_field, ['system' => true, 'read_only' => true]);
- } else {
- $f = $this->owner->addField($this->master_field, ['system' => true, 'read_only' => true]);
- }
- $this->master_field = $f->short_name;
- }
- }
-
- $this->owner->onHook(Model::HOOK_BEFORE_INSERT, \Closure::fromCallable([$this, 'beforeInsert']), [], -5);
- $this->owner->onHook(Model::HOOK_BEFORE_UPDATE, \Closure::fromCallable([$this, 'beforeUpdate']));
- $this->owner->onHook(Model::HOOK_AFTER_DELETE, \Closure::fromCallable([$this, 'doDelete']));
- $this->owner->onHook(Model::HOOK_AFTER_LOAD, \Closure::fromCallable([$this, 'afterLoad']));
- }
- }
-
- /**
- * Returns DSQL query.
- *
- * @return \atk4\dsql\Query
- */
- public function dsql()
- {
- $dsql = $this->owner->persistence->initQuery($this->owner);
- $dsql->reset('table');
- $dsql->table($this->foreign_table, $this->foreign_alias);
-
- return $dsql;
- }
-
- /**
- * Before query is executed, this method will be called.
- *
- * @param Model $model
- * @param \atk4\dsql\Query $query
- */
- public function initSelectQuery($model, $query)
- {
- // if ON is set, we don't have to worry about anything
- if ($this->on) {
- $query->join(
- $this->foreign_table,
- $this->on instanceof \atk4\dsql\Expression ? $this->on : $model->expr($this->on),
- $this->kind,
- $this->foreign_alias
- );
-
- return;
- }
-
- $query->join(
- $this->foreign_table,
- $model->expr('{{}}.{} = {}', [
- ($this->foreign_alias ?: $this->foreign_table),
- $this->foreign_field,
- $this->owner->getField($this->master_field),
- ]),
- $this->kind,
- $this->foreign_alias
- );
-
- /*
- if ($this->reverse) {
- $query->field([$this->short_name => ($this->join ?:
- (
- ($this->owner->table_alias ?: $this->owner->table)
- .'.'.$this->master_field)
- )]);
- } else {
- $query->field([$this->short_name => $this->foreign_alias.'.'.$this->foreign_field]);
- }
- */
- }
-
- /**
- * Called from afterLoad hook.
- *
- * @param Model $model
- */
- public function afterLoad($model)
- {
- // we need to collect ID
- if (isset($model->data[$this->short_name])) {
- $this->id = $model->data[$this->short_name];
- unset($model->data[$this->short_name]);
- }
- }
-
- /**
- * Called from beforeInsert hook.
- *
- * @param Model $model
- * @param array $data
- */
- public function beforeInsert($model, &$data)
- {
- if ($this->weak) {
- return;
- }
-
- // The value for the master_field is set, so we are going to use existing record anyway
- if ($model->hasField($this->master_field) && $model->get($this->master_field)) {
- return;
- }
-
- $insert = $this->dsql();
- $insert->mode('insert');
- $insert->set($model->persistence->typecastSaveRow($model, $this->save_buffer));
- $this->save_buffer = [];
- $insert->set($this->foreign_field, null);
- $insert->insert();
- $this->id = $this->owner->persistence->lastInsertId($this->owner);
-
- if ($this->join) {
- $this->join->set($this->master_field, $this->id);
- } else {
- $data[$this->master_field] = $this->id;
- }
- }
-
- /**
- * Called from afterInsert hook.
- *
- * @param Model $model
- * @param mixed $id
- */
- public function afterInsert($model, $id)
- {
- if ($this->weak) {
- return;
- }
-
- $insert = $this->dsql();
- $insert->set($model->persistence->typecastSaveRow($model, $this->save_buffer));
- $this->save_buffer = [];
- $insert
- ->set(
- $this->foreign_field,
- isset($this->join) ? $this->join->id : $id
- );
- $insert->insert();
- $this->id = $this->owner->persistence->lastInsertId($this->owner);
- }
-
- /**
- * Called from beforeUpdate hook.
- *
- * @param Model $model
- * @param array $data
- */
- public function beforeUpdate($model, &$data)
- {
- if ($this->weak) {
- return;
- }
-
- if (!$this->save_buffer) {
- return;
- }
-
- $update = $this->dsql();
- $update->set($model->persistence->typecastSaveRow($model, $this->save_buffer));
- $this->save_buffer = [];
-
- if ($this->reverse) {
- $update->where($this->foreign_field, $model->id);
- } else {
- $update->where($this->foreign_field, $model->get($this->master_field));
- }
-
- $update->update();
- }
-
- /**
- * Called from beforeDelete and afterDelete hooks.
- *
- * @param Model $model
- * @param mixed $id
- */
- public function doDelete($model, $id)
- {
- if ($this->weak) {
- return;
- }
-
- $delete = $this->dsql();
- if ($this->reverse) {
- $delete->where($this->foreign_field, $this->owner->id);
- } else {
- $delete->where($this->foreign_field, $this->owner->get($this->master_field));
- }
-
- $delete->delete()->execute();
- }
}
diff --git a/src/Model.php b/src/Model.php
index 46a6e91e98..eb2ea75360 100644
--- a/src/Model.php
+++ b/src/Model.php
@@ -34,6 +34,7 @@ class Model implements \IteratorAggregate
use CollectionTrait;
use ReadableCaptionTrait;
use Model\ReferencesTrait;
+ use Model\JoinsTrait;
use Model\UserActionsTrait;
/** @const string */
@@ -93,13 +94,6 @@ class Model implements \IteratorAggregate
*/
public $_default_seed_addExpression = [Field\Callback::class];
- /**
- * The class used by join() method.
- *
- * @var string|array
- */
- public $_default_seed_join = [Join::class];
-
/**
* @var array Collection containing Field Objects - using key as the field system name
*/
@@ -1896,56 +1890,6 @@ public function action($mode, $args = [])
// }}}
- // {{{ Join support
-
- /**
- * Creates an objects that describes relationship between multiple tables (or collections).
- *
- * When object is loaded, then instead of pulling all the data from a single table,
- * join will also query $foreign_table in order to find additional fields. When inserting
- * the record will be also added inside $foreign_table and relationship will be maintained.
- *
- * @param array $defaults
- *
- * @return Join
- */
- public function join(string $foreign_table, $defaults = [])
- {
- if (!is_array($defaults)) {
- $defaults = ['master_field' => $defaults];
- } elseif (isset($defaults[0])) {
- $defaults['master_field'] = $defaults[0];
- unset($defaults[0]);
- }
-
- $defaults[0] = $foreign_table;
-
- $c = $this->_default_seed_join;
-
- return $this->add($this->factory($c, $defaults));
- }
-
- /**
- * Left Join support.
- *
- * @see join()
- *
- * @param array $defaults
- *
- * @return Join
- */
- public function leftJoin(string $foreign_table, $defaults = [])
- {
- if (!is_array($defaults)) {
- $defaults = ['master_field' => $defaults];
- }
- $defaults['weak'] = true;
-
- return $this->join($foreign_table, $defaults);
- }
-
- // }}}
-
// {{{ Expressions
/**
diff --git a/src/Model/Join.php b/src/Model/Join.php
new file mode 100644
index 0000000000..6bec7fd451
--- /dev/null
+++ b/src/Model/Join.php
@@ -0,0 +1,470 @@
+persistence.
+ *
+ * @var \atk4\data\Persistence
+ */
+ protected $persistence;
+
+ /**
+ * ID used by a joined table.
+ *
+ * @var mixed
+ */
+ protected $id;
+
+ /**
+ * Field that is used as native "ID" in the foreign table.
+ * When deleting record, this field will be conditioned.
+ *
+ * ->where($join->id_field, $join->id)->delete();
+ *
+ * @var string
+ */
+ protected $id_field = 'id';
+
+ /**
+ * By default this will be either "inner" (for strong) or "left" for weak joins.
+ * You can specify your own type of join by passing ['kind'=>'right']
+ * as second argument to join().
+ *
+ * @var string
+ */
+ protected $kind;
+
+ /**
+ * Is our join weak? Weak join will stop you from touching foreign table.
+ *
+ * @var bool
+ */
+ protected $weak = false;
+
+ /**
+ * Normally the foreign table is saved first, then it's ID is used in the
+ * primary table. When deleting, the primary table record is deleted first
+ * which is followed by the foreign table record.
+ *
+ * If you are using the following syntax:
+ *
+ * $user->join('contact','default_contact_id');
+ *
+ * Then the ID connecting tables is stored in foreign table and the order
+ * of saving and delete needs to be reversed. In this case $reverse
+ * will be set to `true`. You can specify value of this property.
+ *
+ * @var bool
+ */
+ protected $reverse;
+
+ /**
+ * Field to be used for matching inside master field. By default
+ * it's $foreign_table.'_id'.
+ *
+ * @var string
+ */
+ protected $master_field;
+
+ /**
+ * Field to be used for matching in a foreign table. By default
+ * it's 'id'.
+ *
+ * @var string
+ */
+ protected $foreign_field;
+
+ /**
+ * A short symbolic name that will be used as an alias for the joined table.
+ *
+ * @var string
+ */
+ public $foreign_alias;
+
+ /**
+ * When $prefix is set, then all the fields generated through
+ * our wrappers will be automatically prefixed inside the model.
+ *
+ * @var string
+ */
+ protected $prefix = '';
+
+ /**
+ * Data which is populated here as the save/insert progresses.
+ *
+ * @var array
+ */
+ protected $save_buffer = [];
+
+ /**
+ * When join is done on another join.
+ *
+ * @var Join
+ */
+ protected $join;
+
+ /**
+ * Default constructor. Will copy argument into properties.
+ *
+ * @param array $defaults
+ */
+ public function __construct($foreign_table = null)
+ {
+ if ($foreign_table !== null) {
+ $this->foreign_table = $foreign_table;
+ }
+ }
+
+ /**
+ * Will use either foreign_alias or create #join_.
+ */
+ public function getDesiredName(): string
+ {
+ return '#join_' . $this->foreign_table;
+ }
+
+ /**
+ * Initialization.
+ */
+ public function init(): void
+ {
+ $this->_init();
+
+ // handle foreign table containing a dot
+ if (is_string($this->foreign_table) && strpos($this->foreign_table, '.') !== false) {
+ if (!isset($this->reverse)) {
+ $this->reverse = true;
+ if (isset($this->master_field)) {
+ // both master and foreign fields are set
+
+ // master_field exists, no we will use that
+ // if (!is_object($this->master_field) && !$this->owner->hasField($this->master_field)) {
+ throw (new Exception('You are trying to link tables on non-id fields. This is not implemented yet'))
+ ->addMoreInfo('condition', $this->owner->table . '.' . $this->master_field . ' = ' . $this->foreign_table);
+ // } $this->reverse = 'link';
+ }
+ }
+
+ // split by LAST dot in foreign_table name
+ [$this->foreign_table, $this->foreign_field] = preg_split('/\.+(?=[^\.]+$)/', $this->foreign_table);
+
+ if (!$this->master_field) {
+ $this->master_field = 'id';
+ }
+ } else {
+ $this->reverse = false;
+ $id_field = $this->owner->id_field ?: 'id';
+ if (!$this->master_field) {
+ $this->master_field = $this->foreign_table . '_' . $id_field;
+ }
+
+ if (!$this->foreign_field) {
+ $this->foreign_field = $id_field;
+ }
+ }
+
+ $this->owner->onHook(Model::HOOK_AFTER_UNLOAD, \Closure::fromCallable([$this, 'afterUnload']));
+ }
+
+ /**
+ * Adding field into join will automatically associate that field
+ * with this join. That means it won't be loaded from $table, but
+ * form the join instead.
+ *
+ * @param string $name
+ * @param array $seed
+ *
+ * @return \atk4\data\Field
+ */
+ public function addField($name, $seed = [])
+ {
+ if ($seed && !is_array($seed)) {
+ $seed = [$seed];
+ }
+ $seed['join'] = $this;
+
+ return $this->owner->addField($this->prefix . $name, $seed);
+ }
+
+ /**
+ * Adds multiple fields.
+ *
+ * @param array $fields
+ *
+ * @return $this
+ */
+ public function addFields($fields = [])
+ {
+ foreach ($fields as $field) {
+ if (is_array($field)) {
+ $name = $field[0];
+ unset($field[0]);
+ $this->addField($name, $field);
+ } else {
+ $this->addField($field);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Adds any object to owner model.
+ */
+ public function add(object $object, array $defaults = []): object
+ {
+ if (!is_array($defaults)) {
+ $defaults = ['name' => $defaults];
+ }
+
+ $defaults['join'] = $this;
+
+ return $this->owner->add($object, $defaults);
+ }
+
+ /**
+ * Another join will be attached to a current join.
+ *
+ * @param array $defaults
+ *
+ * @return static
+ */
+ public function join(string $foreign_table, $defaults = [])
+ {
+ if (!is_array($defaults)) {
+ $defaults = ['master_field' => $defaults];
+ }
+ $defaults['join'] = $this;
+
+ return $this->owner->join($foreign_table, $defaults);
+ }
+
+ /**
+ * Another leftJoin will be attached to a current join.
+ *
+ * @param array $defaults
+ *
+ * @return Join
+ */
+ public function leftJoin(string $foreign_table, $defaults = [])
+ {
+ if (!is_array($defaults)) {
+ $defaults = ['master_field' => $defaults];
+ }
+ $defaults['join'] = $this;
+
+ return $this->owner->leftJoin($foreign_table, $defaults);
+ }
+
+ /**
+ * weakJoin will be attached to a current join.
+ *
+ * @todo NOT IMPLEMENTED! weakJoin method does not exist!
+ *
+ * @param array $defaults
+ *
+ * @return
+ */
+ /*
+ public function weakJoin($defaults = [])
+ {
+ $defaults['join'] = $this;
+
+ return $this->owner->weakJoin($defaults);
+ }
+ */
+
+ /**
+ * Creates reference based on a field from the join.
+ *
+ * @param string $link
+ * @param array $defaults
+ *
+ * @return Reference\HasOne
+ */
+ public function hasOne($link, $defaults = [])
+ {
+ if (!is_array($defaults)) {
+ $defaults = ['model' => $defaults ?: 'Model_' . $link];
+ }
+
+ $defaults['join'] = $this;
+
+ return $this->owner->hasOne($link, $defaults);
+ }
+
+ /**
+ * Creates reference based on the field from the join.
+ *
+ * @param string $link
+ * @param array $defaults
+ *
+ * @return Reference\HasMany
+ */
+ public function hasMany($link, $defaults = [])
+ {
+ if (!is_array($defaults)) {
+ $defaults = ['model' => $defaults ?: 'Model_' . $link];
+ }
+
+ $defaults = array_merge([
+ 'our_field' => $this->id_field,
+ 'their_field' => $this->owner->table . '_' . $this->id_field,
+ ], $defaults);
+
+ return $this->owner->hasMany($link, $defaults);
+ }
+
+ /**
+ * Wrapper for containsOne that will associate field
+ * with join.
+ *
+ * @todo NOT IMPLEMENTED !
+ *
+ * @param Model $model
+ * @param array $defaults
+ *
+ * @return ???
+ */
+ /*
+ public function containsOne($model, $defaults = [])
+ {
+ if (!is_array($defaults)) {
+ $defaults = [$defaults];
+ }
+
+ if (is_string($defaults[0])) {
+ $defaults[0] = $this->addField($defaults[0], ['system' => true]);
+ }
+
+ return parent::containsOne($model, $defaults);
+ }
+ */
+
+ /**
+ * Wrapper for containsMany that will associate field
+ * with join.
+ *
+ * @todo NOT IMPLEMENTED !
+ *
+ * @param Model $model
+ * @param array $defaults
+ *
+ * @return ???
+ */
+ /*
+ public function containsMany($model, $defaults = [])
+ {
+ if (!is_array($defaults)) {
+ $defaults = [$defaults];
+ }
+
+ if (is_string($defaults[0])) {
+ $defaults[0] = $this->addField($defaults[0], ['system' => true]);
+ }
+
+ return parent::containsMany($model, $defaults);
+ }
+ */
+
+ /**
+ * Will iterate through this model by pulling
+ * - fields
+ * - references
+ * - conditions.
+ *
+ * and then will apply them locally. If you think that any fields
+ * could clash, then use ['prefix'=>'m2'] which will be pre-pended
+ * to all the fields. Conditions will be automatically mapped.
+ *
+ * @todo NOT IMPLEMENTED !
+ *
+ * @param Model $model
+ * @param array $defaults
+ */
+ /*
+ public function importModel($model, $defaults = [])
+ {
+ // not implemented yet !!!
+ }
+ */
+
+ /**
+ * Joins with the primary table of the model and
+ * then import all of the data into our model.
+ *
+ * @todo NOT IMPLEMENTED!
+ *
+ * @param Model $model
+ * @param array $fields
+ */
+ /*
+ public function weakJoinModel($model, $fields = [])
+ {
+ if (!is_object($model)) {
+ $model = $this->owner->connection->add($model);
+ }
+ $j = $this->join($model->table);
+
+ $j->importModel($model);
+
+ return $j;
+ }
+ */
+
+ /**
+ * Set value.
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function set($field, $value)
+ {
+ $this->save_buffer[$field] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Clears id and save buffer.
+ */
+ protected function afterUnload()
+ {
+ $this->id = null;
+ $this->save_buffer = [];
+ }
+}
diff --git a/src/Model/JoinsTrait.php b/src/Model/JoinsTrait.php
new file mode 100644
index 0000000000..5bbe54d6c5
--- /dev/null
+++ b/src/Model/JoinsTrait.php
@@ -0,0 +1,58 @@
+ $defaults];
+ } elseif (isset($defaults[0])) {
+ $defaults['master_field'] = $defaults[0];
+ unset($defaults[0]);
+ }
+
+ $defaults[0] = $foreignTable;
+
+ return $this->add($this->factory($this->_default_seed_join, $defaults));
+ }
+
+ /**
+ * Left Join support.
+ *
+ * @see join()
+ *
+ * @param array $defaults
+ */
+ public function leftJoin(string $foreignTable, $defaults = []): Join
+ {
+ if (!is_array($defaults)) {
+ $defaults = ['master_field' => $defaults];
+ }
+ $defaults['weak'] = true;
+
+ return $this->join($foreignTable, $defaults);
+ }
+}
diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php
index 421760ee59..dbd8a006cf 100644
--- a/src/Persistence/Array_.php
+++ b/src/Persistence/Array_.php
@@ -53,7 +53,7 @@ public function add(Model $model, array $defaults = []): Model
}
$defaults = array_merge([
- '_default_seed_join' => [\atk4\data\Join\Array_::class],
+ '_default_seed_join' => [Array_\Join::class],
], $defaults);
$model = parent::add($model, $defaults);
diff --git a/src/Persistence/Array_/Join.php b/src/Persistence/Array_/Join.php
new file mode 100644
index 0000000000..bfe628ed6d
--- /dev/null
+++ b/src/Persistence/Array_/Join.php
@@ -0,0 +1,163 @@
+kind)) {
+ $this->kind = $this->weak ? 'left' : 'inner';
+ }
+
+ // Add necessary hooks
+ if ($this->reverse) {
+ $this->owner->onHook(Model::HOOK_AFTER_INSERT, \Closure::fromCallable([$this, 'afterInsert']), [], -5);
+ $this->owner->onHook(Model::HOOK_BEFORE_UPDATE, \Closure::fromCallable([$this, 'beforeUpdate']), [], -5);
+ $this->owner->onHook(Model::HOOK_BEFORE_DELETE, \Closure::fromCallable([$this, 'doDelete']), [], -5);
+ } else {
+ $this->owner->onHook(Model::HOOK_BEFORE_INSERT, \Closure::fromCallable([$this, 'beforeInsert']));
+ $this->owner->onHook(Model::HOOK_BEFORE_UPDATE, \Closure::fromCallable([$this, 'beforeUpdate']));
+ $this->owner->onHook(Model::HOOK_AFTER_DELETE, \Closure::fromCallable([$this, 'doDelete']));
+ $this->owner->onHook(Model::HOOK_AFTER_LOAD, \Closure::fromCallable([$this, 'afterLoad']));
+ }
+ }
+
+ /**
+ * Called from afterLoad hook.
+ *
+ * @param Model $model
+ */
+ public function afterLoad($model)
+ {
+ // we need to collect ID
+ $this->id = $model->data[$this->master_field];
+ if (!$this->id) {
+ return;
+ }
+
+ try {
+ $data = $model->persistence->load($model, $this->id, $this->foreign_table);
+ } catch (Exception $e) {
+ throw (new Exception('Unable to load joined record', $e->getCode(), $e))
+ ->addMoreInfo('table', $this->foreign_table)
+ ->addMoreInfo('id', $this->id);
+ }
+ $model->data = array_merge($data, $model->data);
+ }
+
+ /**
+ * Called from beforeInsert hook.
+ *
+ * @param Model $model
+ * @param array $data
+ */
+ public function beforeInsert($model, &$data)
+ {
+ if ($this->weak) {
+ return;
+ }
+
+ if ($model->hasField($this->master_field) && $model->get($this->master_field)) {
+ // The value for the master_field is set,
+ // we are going to use existing record.
+ return;
+ }
+
+ // Figure out where are we going to save data
+ $persistence = $this->persistence ?:
+ $this->owner->persistence;
+
+ $this->id = $persistence->insert(
+ $model,
+ $this->save_buffer,
+ $this->foreign_table
+ );
+
+ $data[$this->master_field] = $this->id;
+
+ //$this->owner->set($this->master_field, $this->id);
+ }
+
+ /**
+ * Called from afterInsert hook.
+ *
+ * @param Model $model
+ * @param mixed $id
+ */
+ public function afterInsert($model, $id)
+ {
+ if ($this->weak) {
+ return;
+ }
+
+ $this->save_buffer[$this->foreign_field] = isset($this->join) ? $this->join->id : $id;
+
+ $persistence = $this->persistence ?: $this->owner->persistence;
+
+ $this->id = $persistence->insert(
+ $model,
+ $this->save_buffer,
+ $this->foreign_table
+ );
+ }
+
+ /**
+ * Called from beforeUpdate hook.
+ *
+ * @param Model $model
+ * @param array $data
+ */
+ public function beforeUpdate($model, &$data)
+ {
+ if ($this->weak) {
+ return;
+ }
+
+ $persistence = $this->persistence ?: $this->owner->persistence;
+
+ $this->id = $persistence->update(
+ $model,
+ $this->id,
+ $this->save_buffer,
+ $this->foreign_table
+ );
+ }
+
+ /**
+ * Called from beforeDelete and afterDelete hooks.
+ *
+ * @param Model $model
+ * @param mixed $id
+ */
+ public function doDelete($model, $id)
+ {
+ if ($this->weak) {
+ return;
+ }
+
+ $persistence = $this->persistence ?: $this->owner->persistence;
+
+ $persistence->delete(
+ $model,
+ $this->id,
+ $this->foreign_table
+ );
+
+ $this->id = null;
+ }
+}
diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php
index e628a6a06f..4fdbac4b26 100644
--- a/src/Persistence/Sql.php
+++ b/src/Persistence/Sql.php
@@ -71,7 +71,7 @@ class Sql extends Persistence
*
* @var string
*/
- public $_default_seed_join = [\atk4\data\Join\Sql::class];
+ public $_default_seed_join = [Sql\Join::class];
/**
* Constructor.
diff --git a/src/Persistence/Sql/Join.php b/src/Persistence/Sql/Join.php
new file mode 100644
index 0000000000..94461292e0
--- /dev/null
+++ b/src/Persistence/Sql/Join.php
@@ -0,0 +1,272 @@
+.
+ */
+ public function getDesiredName(): string
+ {
+ return '_' . ($this->foreign_alias ?: $this->foreign_table[0]);
+ }
+
+ /**
+ * Returns DSQL Expression.
+ *
+ * @param \atk4\dsql\Expression $q
+ *
+ * @return \atk4\dsql\Expression
+ */
+ public function getDsqlExpression($q)
+ {
+ /*
+ // If our Model has expr() method (inherited from Persistence\Sql) then use it
+ if ($this->owner->hasMethod('expr')) {
+ return $this->owner->expr('{}.{}', [$this->foreign_alias, $this->foreign_field]);
+ }
+
+ // Otherwise call it from expression itself
+ return $q->expr('{}.{}', [$this->foreign_alias, $this->foreign_field]);
+ */
+
+ // Romans: Join\Sql shouldn't even be called if expr is undefined. I think we should leave it here to produce error.
+ return $this->owner->expr('{}.{}', [$this->foreign_alias, $this->foreign_field]);
+ }
+
+ /**
+ * This method is to figure out stuff.
+ */
+ public function init(): void
+ {
+ parent::init();
+
+ $this->owner->persistence_data['use_table_prefixes'] = true;
+
+ // If kind is not specified, figure out join type
+ if (!isset($this->kind)) {
+ $this->kind = $this->weak ? 'left' : 'inner';
+ }
+
+ // Our short name will be unique
+ if (!$this->foreign_alias) {
+ $this->foreign_alias = ($this->owner->table_alias ?: '') . $this->short_name;
+ }
+
+ $this->owner->onHook(Persistence\Sql::HOOK_INIT_SELECT_QUERY, \Closure::fromCallable([$this, 'initSelectQuery']));
+
+ // Add necessary hooks
+ if ($this->reverse) {
+ $this->owner->onHook(Model::HOOK_AFTER_INSERT, \Closure::fromCallable([$this, 'afterInsert']));
+ $this->owner->onHook(Model::HOOK_BEFORE_UPDATE, \Closure::fromCallable([$this, 'beforeUpdate']));
+ $this->owner->onHook(Model::HOOK_BEFORE_DELETE, \Closure::fromCallable([$this, 'doDelete']), [], -5);
+ $this->owner->onHook(Model::HOOK_AFTER_LOAD, \Closure::fromCallable([$this, 'afterLoad']));
+ } else {
+ // Master field indicates ID of the joined item. In the past it had to be
+ // defined as a physical field in the main table. Now it is a model field
+ // so you can use expressions or fields inside joined entities.
+ // If string specified here does not point to an existing model field
+ // a new basic field is inserted and marked hidden.
+ if (is_string($this->master_field)) {
+ if (!$this->owner->hasField($this->master_field)) {
+ if ($this->join) {
+ $f = $this->join->addField($this->master_field, ['system' => true, 'read_only' => true]);
+ } else {
+ $f = $this->owner->addField($this->master_field, ['system' => true, 'read_only' => true]);
+ }
+ $this->master_field = $f->short_name;
+ }
+ }
+
+ $this->owner->onHook(Model::HOOK_BEFORE_INSERT, \Closure::fromCallable([$this, 'beforeInsert']), [], -5);
+ $this->owner->onHook(Model::HOOK_BEFORE_UPDATE, \Closure::fromCallable([$this, 'beforeUpdate']));
+ $this->owner->onHook(Model::HOOK_AFTER_DELETE, \Closure::fromCallable([$this, 'doDelete']));
+ $this->owner->onHook(Model::HOOK_AFTER_LOAD, \Closure::fromCallable([$this, 'afterLoad']));
+ }
+ }
+
+ /**
+ * Returns DSQL query.
+ *
+ * @return \atk4\dsql\Query
+ */
+ public function dsql()
+ {
+ $dsql = $this->owner->persistence->initQuery($this->owner);
+
+ return $dsql->reset('table')->table($this->foreign_table, $this->foreign_alias);
+ }
+
+ /**
+ * Before query is executed, this method will be called.
+ *
+ * @param Model $model
+ * @param \atk4\dsql\Query $query
+ */
+ public function initSelectQuery(Model $model, Query $query)
+ {
+ // if ON is set, we don't have to worry about anything
+ if ($this->on) {
+ $query->join(
+ $this->foreign_table,
+ $this->on instanceof \atk4\dsql\Expression ? $this->on : $model->expr($this->on),
+ $this->kind,
+ $this->foreign_alias
+ );
+
+ return;
+ }
+
+ $query->join(
+ $this->foreign_table,
+ $model->expr('{{}}.{} = {}', [
+ ($this->foreign_alias ?: $this->foreign_table),
+ $this->foreign_field,
+ $this->owner->getField($this->master_field),
+ ]),
+ $this->kind,
+ $this->foreign_alias
+ );
+
+ /*
+ if ($this->reverse) {
+ $query->field([$this->short_name => ($this->join ?:
+ (
+ ($this->owner->table_alias ?: $this->owner->table)
+ .'.'.$this->master_field)
+ )]);
+ } else {
+ $query->field([$this->short_name => $this->foreign_alias.'.'.$this->foreign_field]);
+ }
+ */
+ }
+
+ /**
+ * Called from afterLoad hook.
+ *
+ * @param Model $model
+ */
+ public function afterLoad(Model $model)
+ {
+ // we need to collect ID
+ if (isset($model->data[$this->short_name])) {
+ $this->id = $model->data[$this->short_name];
+ unset($model->data[$this->short_name]);
+ }
+ }
+
+ /**
+ * Called from beforeInsert hook.
+ *
+ * @param Model $model
+ * @param array $data
+ */
+ public function beforeInsert(Model $model, &$data)
+ {
+ if ($this->weak) {
+ return;
+ }
+
+ // The value for the master_field is set, so we are going to use existing record anyway
+ if ($model->hasField($this->master_field) && $model->get($this->master_field)) {
+ return;
+ }
+
+ $query = $this->dsql();
+ $query->mode('insert');
+ $query->set($model->persistence->typecastSaveRow($model, $this->save_buffer));
+ $this->save_buffer = [];
+ $query->set($this->foreign_field, null);
+ $query->insert();
+ $this->id = $this->owner->persistence->lastInsertId($this->owner);
+
+ if ($this->join) {
+ $this->join->set($this->master_field, $this->id);
+ } else {
+ $data[$this->master_field] = $this->id;
+ }
+ }
+
+ /**
+ * Called from afterInsert hook.
+ *
+ * @param Model $model
+ * @param mixed $id
+ */
+ public function afterInsert(Model $model, $id)
+ {
+ if ($this->weak) {
+ return;
+ }
+
+ $query = $this->dsql();
+ $query->set($model->persistence->typecastSaveRow($model, $this->save_buffer));
+ $this->save_buffer = [];
+ $query->set($this->foreign_field, $this->join->id ?? $id);
+ $query->insert();
+ $this->id = $this->owner->persistence->lastInsertId($this->owner);
+ }
+
+ /**
+ * Called from beforeUpdate hook.
+ *
+ * @param Model $model
+ * @param array $data
+ */
+ public function beforeUpdate(Model $model, &$data)
+ {
+ if ($this->weak) {
+ return;
+ }
+
+ if (!$this->save_buffer) {
+ return;
+ }
+
+ $query = $this->dsql();
+ $query->set($model->persistence->typecastSaveRow($model, $this->save_buffer));
+ $this->save_buffer = [];
+
+ $id = $this->reverse ? $model->id : $model->get($this->master_field);
+
+ $query->where($this->foreign_field, $id)->update();
+ }
+
+ /**
+ * Called from beforeDelete and afterDelete hooks.
+ *
+ * @param Model $model
+ * @param mixed $id
+ */
+ public function doDelete(Model $model, $id)
+ {
+ if ($this->weak) {
+ return;
+ }
+
+ $id = $this->reverse ? $this->owner->id : $this->owner->get($this->master_field);
+
+ $this->dsql()->where($this->foreign_field, $id)->delete();
+ }
+}