diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php
index 66b3b3a..be4274e 100644
--- a/src/ActiveRecord.php
+++ b/src/ActiveRecord.php
@@ -1,138 +1,48 @@
*/
namespace Evas\Orm;
-use Evas\Base\App;
use Evas\Base\Help\HooksTrait;
-use Evas\Db\Interfaces\DatabaseInterface;
-use Evas\Db\Interfaces\QueryBuilderInterface;
-use Evas\Db\Table;
use Evas\Orm\Exceptions\LastInsertIdUndefinedException;
+use Evas\Orm\Traits\ActiveRecordConvertTrait;
+use Evas\Orm\Traits\ActiveRecordDatabaseTrait;
+use Evas\Orm\Traits\ActiveRecordDataTrait;
+use Evas\Orm\Traits\ActiveRecordRelatedsTrait;
+use Evas\Orm\Traits\ActiveRecordRelationsTrait;
+use Evas\Orm\Traits\ActiveRecordStateTrait;
+use Evas\Orm\Traits\ActiveRecordTableTrait;
+use Evas\Orm\Traits\ActiveRecordQueryTrait;
-abstract class ActiveRecord
-{
- // подключаем поддержку произвольных хуков в наследуемых классах
- use HooksTrait;
+use Evas\Orm\Traits\ActiveRecordIdentityTrait;
- /** @var string имя соединения с базой данных*/
- public static $dbname;
- /** @var string имя соединения с базой данных только для записи */
- public static $dbnameWrite;
- /*** @var string кастомное имя таблицы */
- public static $tableName;
- /**
- * Получение соединения с базой данных.
- * @param bool использовать ли соединение для записи
- * @return DatabaseInterface
- */
- // abstract public static function getDb(): DatabaseInterface;
- public static function getDb(bool $write = false): DatabaseInterface
- {
- $dbname = static::getDbName($write);
- return App::db($dbname);
- }
+class ActiveRecord implements \JsonSerializable
+{
+ // подключаем конвертацию
+ use ActiveRecordConvertTrait;
- /**
- * Получение имени соединения с базой данных.
- * @param bool использовать ли соединение для записи
- * @return string|null
- */
- public static function getDbName(bool $write = false): ?string
- {
- return true === $write && !empty(static::$dbnameWrite)
- ? static::$dbnameWrite : static::$dbname;
- }
+ use ActiveRecordDatabaseTrait;
- /**
- * Генерация имени таблицы из имени класса.
- * @return string
- */
- public static function generateTableName(): string
- {
- $className = get_called_class();
- $lastSlash = strrpos($className, '\\');
- if ($lastSlash > 0) {
- $className = substr($className, $lastSlash + 1);
- }
- $mapperPos = strrpos($className, 'Mapper');
- if (false !== $mapperPos) {
- $className = substr($className, 0, $mapperPos);
- }
- return strtolower(preg_replace('/([a-z0-9]+)([A-Z]{1})/', '$1_$2', $className)) . 's';
- }
+ use ActiveRecordDataTrait;
- /**
- * Получение имени таблицы из маппинга моделей таблиц.
- * @return string|null имя таблицы
- */
- public static function tableNameFromMap(): ?string
- {
- return static::getDb()->modelTablesMap()->getModelTable(get_called_class());
- }
+ use ActiveRecordRelatedsTrait;
- /**
- * Получение имени таблицы.
- * @return string
- */
- public static function tableName(): string
- {
- if (empty(static::$tableName)) {
- static::$tableName = static::generateTableName();
- }
- return static::$tableName;
- }
+ use ActiveRecordRelationsTrait;
- /**
- * Получение объекта таблицы.
- * @return Table
- */
- public static function table(): Table
- {
- return static::getDb()->table(static::tableName());
- }
+ use ActiveRecordStateTrait;
- /**
- * Получение первичного ключа.
- * @param bool|null переполучить из схемы заново
- * @return string
- */
- public static function primaryKey(bool $reload = false): string
- {
- return static::table()->primaryKey($reload);
- }
+ use ActiveRecordTableTrait;
- /**
- * Получение столбцов таблицы.
- * @param bool|null переполучить из схемы заново
- * @return array
- */
- public static function columns(bool $reload = false): array
- {
- return static::table()->columns($reload);
- }
+ use ActiveRecordQueryTrait;
- /**
- * Получение id последней записи.
- * @return int|null
- */
- public static function lastInsertId(): ?int
- {
- return static::table()->lastInsertId();
- }
+ // подключаем поддержку произвольных хуков в наследуемых классах
+ use HooksTrait;
- /**
- * Дефолтные значения новой записи.
- * @return array
- */
- public static function default(): array
- {
- return [];
- }
+ use ActiveRecordIdentityTrait;
/**
* Конструктор.
@@ -140,88 +50,17 @@ public static function default(): array
*/
public function __construct(array $props = null)
{
+ $creating = empty($this->primaryValue());
+ if (!$creating) $this->saveState();
$this->hook('beforeConstruct', $props);
-
- $pk = static::primaryKey();
- $creating = empty($props[$pk]);
-
- if ($creating) $this->hook('beforeCreate', $props);
- else $this->hook('beforeGet', $props);
+ $this->hook($creating ? 'beforeCreate' : 'beforeGet', $props);
if (!empty($props)) $this->fill($props);
- if ($creating) $this->hook('afterCreate');
- else $this->hook('afterGet');
+ $this->hook($creating ? 'afterCreate' : 'afterGet');
$this->hook('afterConstruct');
}
- /**
- * Заполнение модели свойствами.
- * @param array свойства модели
- * @return self
- */
- public function fill(array $props): ActiveRecord
- {
- foreach ($props as $name => $value) {
- $this->$name = $value;
- }
- return $this;
- }
-
-
- /**
- * Получение маппинга данных записи для базы данных.
- * @return array
- */
- public function getRowProperties(): array
- {
- $props = [];
- foreach (static::columns() as &$column) {
- if (isset($this->$column)) {
- $props[$column] = $this->$column;
- }
- }
- return $props;
- }
-
- /**
- * Получение состояния сохраняемых полей.
- * @return array
- */
- public function getState(): array
- {
- $state = static::getDb()->identityMapGetState($this, static::primaryKey());
- foreach ($state as $name => &$value) {
- if (!isset($value) || !in_array($name, static::columns())) {
- unset($state[$name]);
- }
- }
- return $state;
- }
-
- /**
- * Получение маппинга измененных свойств записи.
- * @return array
- */
- public function getUpdatedProperties(): array
- {
- $props = $this->getRowProperties();
- $pk = static::primaryKey();
- if (empty($this->$pk)) {
- return $props;
- } else {
- // $state = static::getDb()->identityMapGetState($this, $pk);
- $state = $this->getState();
- // return array_diff($props, $state ?? []);
-
- return array_merge(
- array_fill_keys(array_keys(array_diff($state ?? [], $props)), null),
- array_diff_assoc($props, $state ?? [])
- );
- }
- }
-
-
/**
* Сохранение записи.
* @return self
@@ -229,29 +68,53 @@ public function getUpdatedProperties(): array
*/
public function save()
{
- if (empty($this->getUpdatedProperties())) {
+ if (empty($this->getUpdatedProps())) {
$this->hook('nothingSave');
return $this;
}
$this->hook('beforeSave');
$pk = static::primaryKey();
- if (empty($this->$pk)) {
- $this->hook('beforeInsert');
- static::getDb(true)->insert(static::tableName(), $this->getUpdatedProperties());
- $this->$pk = static::lastInsertId();
- if (empty($this->$pk)) {
- throw new LastInsertIdUndefinedException();
+ if ($this->isStateHasPrimaryValue()) {
+ $this->hook('beforeUpdate');
+ $props = $this->getUpdatedProps();
+ if (empty($props)) {
+ $this->hook('nothingUpdate');
+ return $this;
+ } else {
+ static::table(true)->where($pk, $this->$pk)->update($props);
+ $this->hook('afterUpdate', $props);
}
- $this->hook('afterInsert');
} else {
- $this->hook('beforeUpdate');
- static::getDb(true)->update(static::tableName(), $this->getUpdatedProperties())
- ->where("$pk = ?", [$this->$pk])->one();
- $this->hook('afterUpdate');
+ $this->hook('beforeInsert');
+ $props = $this->getUpdatedProps();
+ if (empty($props)) {
+ $this->hook('nothingInsert');
+ return $this;
+ } else {
+ static::table(true)->insert($props);
+ $this->$pk = static::lastInsertId();
+ if (empty($this->$pk)) {
+ throw new LastInsertIdUndefinedException();
+ }
+ $this->hook('afterInsert', $props);
+ }
+ }
+ $this->hook('afterSave', $props);
+ // return static::getDb()->identityMapUpdate($this, $pk);
+ return $this->saveState();
+ }
+
+ /**
+ * Сохранение модели и её связей.
+ */
+ public function push()
+ {
+ $this->save();
+ foreach ($this->relatedCollections as $models) {
+ if (!is_array($models)) $models = [$models];
+ foreach ($models as $model) $model->push();
}
- $this->hook('afterSave');
- return static::getDb()->identityMapUpdate($this, $pk);
}
/**
@@ -265,85 +128,24 @@ public function delete()
return $this;
}
$this->hook('beforeDelete');
- $qr = static::getDb(true)->delete(static::tableName())
- ->where("$pk = ?", [$this->$pk])->one();
+ $qr = static::table(true)->where($pk, $this->$pk)->limit(1)->delete();
$this->hook('afterDelete', $qr->rowCount());
if (0 < $qr->rowCount()) {
- static::getDb()->identityMapUnset($this, $pk);
- $this->id = null;
+ $this->identityMapRemove();
+ $this->$pk = null;
+ $this->saveState();
}
return $this;
}
/**
- * Создание модели записи.
- * @param array|null значения записи
- * @return static
- */
- public static function create(array $props = null): ActiveRecord
- {
- return new static($props);
- }
-
- /**
- * Создание модели записи с вставкой в базу.
- * @param array|null значения записи
- * @return static
- */
- public static function insert(array $props = null): ActiveRecord
- {
- return static::create($props)->save();
- }
-
- /**
- * Поиск записи через сборщик запроса.
- * @param string|null столбцы
- * @return QueryBuilderInterface
- */
- public static function find(string $columns = null): QueryBuilderInterface
- {
- return static::getDb()->select(static::tableName(), $columns);
- }
-
- /**
- * Поиск по первичному ключу.
- * @param int|string значение первичного ключа, перечисление
- * @return static|array of static
- */
- public static function findByPK(...$ids)
- {
- $pk = static::primaryKey();
- $qb = static::find();
- if (count($ids) > 1) {
- return $qb->whereIn("`$pk`", $ids)
- ->query(count($ids))->classObjectAll(static::class);
- } else {
- return $qb->where("`$pk` = ?", $ids)
- ->one()->classObject(static::class);
- }
- }
-
- /**
- * Поиск по id, алиас для findByPK.
- * @param int id, перечисление
- * @return static|array of static
- */
- public static function findById(int ...$ids)
- {
- return static::findByPK(...$ids);
- }
-
- /**
- * Поиск по sql-запросу.
- * @param string sql-запрос
- * @param array|null значения запроса для экранирования
- * @return static|static[]
+ * Обновление данных модели из базы.
*/
- public static function query(string $sql, array $values = null)
+ public function reload()
{
- $qr = static::getDb()->query($sql, $values);
- return strstr($qr->stmt()->queryString, 'LIMIT 1')
- ? $qr->classObject(static::class)
- : $qr->classObjectAll(static::class);
+ return ($pv = $this->primaryValue())
+ // ? static::find($pv)
+ ? $this->fill(static::find($pv)->toArray())
+ : $this;
}
}
diff --git a/src/IdentityMap.php b/src/IdentityMap.php
new file mode 100644
index 0000000..4f0d732
--- /dev/null
+++ b/src/IdentityMap.php
@@ -0,0 +1,144 @@
+
+ */
+namespace Evas\Orm;
+
+use Evas\Orm\Identity\ModelIdentity;
+use Evas\Orm\ActiveRecord;
+
+class IdentityMap
+{
+ /** @static self единственный экземляр IdentityMap */
+ protected static $instance;
+ /** @var ActiveRecord[] модели */
+ protected $models = [];
+
+ /**
+ * Получение экземпляра IdentityMap.
+ * @return self
+ */
+ public static function instance()
+ {
+ if (!static::$instance) static::$instance = new static;
+ return static::$instance;
+ }
+
+ /**
+ * Конструктор.
+ */
+ protected function __construct()
+ {
+ $this->resetModels();
+ }
+
+ /**
+ * Очистка моделей.
+ */
+ public function resetModels()
+ {
+ $this->models = [];
+ return $this;
+ }
+
+ /**
+ * Получение количества моделей в IdentityMap.
+ * @return int
+ */
+ public static function count(): int
+ {
+ return count(static::instance()->models);
+ }
+
+ /**
+ * Получение моделей IdentityMap.
+ * @return array
+ */
+ public static function models(): array
+ {
+ return static::instance()->models;
+ }
+
+ /**
+ * Проверка наличия модели в IdentityMap.
+ * @param ActiveRecord модель
+ * @return bool
+ */
+ public static function has(ActiveRecord $model): bool
+ {
+ return isset(static::instance()->models[$model->identity()]);
+ }
+
+ /**
+ * Добавление модели в IdentityMap.
+ * @param ActiveRecord модель
+ * @return self
+ */
+ public static function set(ActiveRecord $model)
+ {
+ static::instance()->models[$model->identity()] = $model;
+ return static::instance();
+ }
+
+ /**
+ * Получение модели из IdentityMap или null.
+ * @param ActiveRecord модель
+ * @return ActiveRecord|null модель или null
+ */
+ public static function get(ActiveRecord $model)
+ {
+ return static::instance()->models[$model->identity()] ?? null;
+ }
+
+ /**
+ * Получение модели с установкой в случае отсутствия.
+ * @param ActiveRecord модель
+ * @return ActiveRecord модель
+ */
+ public static function getWithSave(ActiveRecord $model)
+ {
+ if (!static::has($model)) {
+ static::set($model);
+ return $model;
+ } else {
+ $old = static::get($model);
+ // sync state
+ $props = $old->getUpdatedProps();
+ $old->fill($model->getData());
+ $old->saveState();
+ $old->fill($props);
+ return $old;
+ }
+ }
+
+ /**
+ * Удаление модели из IdentityMap.
+ * @param ActiveRecord модель
+ * @return self
+ */
+ public static function unset(ActiveRecord $model)
+ {
+ unset(static::instance()->models[$model->identity()]);
+ return static::instance();
+ }
+
+ /**
+ * Удаление всех моделей из IdentityMap.
+ * @return self
+ */
+ public static function unsetAll()
+ {
+ return static::instance()->resetModels();
+ }
+
+ /**
+ * Приведение IdentityMap к строке.
+ * @return string
+ */
+ public function __toString()
+ {
+ return json_encode(['models_count' => static::count(), 'models' => $this->models]);
+ }
+}
diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php
new file mode 100644
index 0000000..c13bf42
--- /dev/null
+++ b/src/QueryBuilder.php
@@ -0,0 +1,101 @@
+
+ */
+namespace Evas\Orm;
+
+use Evas\Db\Builders\QueryBuilder as DbQueryBuilder;
+use Evas\Db\Interfaces\DatabaseInterface;
+use Evas\Orm\Traits\QueryBuilderRelationsTrait;
+
+class QueryBuilder extends DbQueryBuilder
+{
+ use QueryBuilderRelationsTrait;
+
+ /** @var string класс модели данных */
+ protected $model;
+
+ /**
+ * Расширяем конструктор передачей класса модели.
+ * @param DatabaseInterface
+ * @param string|null класс модели
+ */
+ public function __construct(DatabaseInterface &$db, string $model = null)
+ {
+ parent::__construct($db);
+ if ($model) $this->fromModel($model);
+ }
+
+ /**
+ * Установка модели и проброс таблицы модели.
+ * @param string модель
+ * @return self
+ * @throws \InvalidArgumentException
+ */
+ public function fromModel(string $model)
+ {
+ if (!class_exists($model, true)) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Class by name "%s" passed to 1 argument of %s() does not exist',
+ $model, __METHOD__
+ ));
+ }
+ $this->model = $model;
+ $this->from($model::tableName());
+ return $this;
+ }
+
+
+ /**
+ * Переопределяем получение первичного ключа таблицы.
+ * @return string
+ */
+ protected function primaryKey(): string
+ {
+ return $this->model::primaryKey();
+ }
+
+ /**
+ * Выполнение select-запроса с получением нескольких записей.
+ * @param array|null столбцы для получения
+ * @return array найденные записи
+ */
+ public function get($columns = null): array
+ {
+ $this->applyRelationsBefore();
+
+ if ($columns) $this->select(...func_get_args());
+ $models = $this->query()->objectAll($this->model);
+ if (!$models) return $models;
+ $ids = [];
+ foreach ($models as &$model) {
+ $model = $model->identityMapSave();
+ if (0 < count($this->withOne)) $this->parseWiths($model);
+ $ids[] = $model->primaryValue();
+ }
+ // $models = array_unique($models);
+ $this->applyRelationsAfter($ids, $models);
+ return $models;
+ }
+
+ /**
+ * Выполнение select-запроса с получением одной записи.
+ * @param array|null столбцы для получения
+ * @return array|null найденная запись
+ */
+ public function one($columns = null)
+ {
+ $this->applyRelationsBefore();
+ if ($columns) $this->select(...func_get_args());
+ $model = $this->limit(1)->query()->object($this->model);
+ if ($model) {
+ if (0 < count($this->withOne)) $this->parseWiths($model);
+ $model = $model->identityMapSave();
+ $this->applyRelationsAfter([$model->primaryValue(), [$model]]);
+ }
+ return $model;
+ // return is_null($model) ? $model : $model->identityMapSave();
+ }
+}
diff --git a/src/RelatedCollection.php b/src/RelatedCollection.php
new file mode 100644
index 0000000..25fe4a3
--- /dev/null
+++ b/src/RelatedCollection.php
@@ -0,0 +1,96 @@
+
+ */
+namespace Evas\Orm;
+
+use Evas\Base\Help\Collection;
+use Evas\Orm\ActiveRecord;
+use Evas\Orm\Relation;
+
+class RelatedCollection extends Collection
+{
+ /** @var ActiveRecord модель */
+ protected $model;
+ /** @var Relation связь */
+ protected $relation;
+ /** @var array значения первичных ключей моделей коллекции */
+ protected $ids = [];
+
+ /**
+ * Получение текущей модели коллекции.
+ * @return ActiveRecord|null модель
+ */
+ public function current(): ?ActiveRecord
+ {
+ return parent::current();
+ }
+
+ /**
+ * Проверка наличия модели в коллекции.
+ * @param int|string значение первичного ключа
+ * @return bool
+ */
+ public function has(int $id): bool
+ {
+ return in_array($id, $this->ids);
+ }
+
+ /**
+ * Добавление элемента в коллекцию.
+ * @param mixed элемент
+ * @return self
+ */
+ public function add($item)
+ {
+ if (!($item instanceof $this->relation->foreignModel)) {
+ if ($item instanceof ActiveRecord) $item = $item->toArray();
+ $item = new $this->relation->foreignModel($item);
+ }
+ if (!($id = $item->primaryValue()) || !$this->has($id)) {
+ $this->ids[] = $id;
+ return parent::add($item);
+ }
+ return $this;
+ }
+
+ /**
+ * Конструктор.
+ * @param ActiveRecord модель
+ * @param Relation связь
+ * @param array|Collection|null элементы для коллекции
+ */
+ public function __construct(ActiveRecord $model, Relation $relation, $items = null)
+ {
+ $this->model = &$model;
+ $this->relation = $relation;
+ parent::__construct($items);
+ }
+
+ /**
+ * Перезапрос связанных записей.
+ * @return self
+ */
+ public function reload()
+ {
+ $this->items = $this->relation->foreignModel::where(
+ $this->relation->foreignKey,
+ $this->model->{$this->relation->localKey}
+ )->get();
+ return $this;
+ }
+
+ /**
+ * Сохраенение связанных записей.
+ * @return self
+ */
+ public function save()
+ {
+ foreach ($this->items as &$item) {
+ $item->save();
+ }
+ return $this;
+ }
+}
diff --git a/src/Relation.php b/src/Relation.php
new file mode 100644
index 0000000..d13d4a2
--- /dev/null
+++ b/src/Relation.php
@@ -0,0 +1,199 @@
+
+ */
+namespace Evas\Orm;
+
+use Evas\Orm\ActiveRecord;
+use Evas\Orm\QueryBuilder;
+
+class Relation
+{
+ /** @var string тип связи */
+ public $type;
+ /** @var string класс локальной модели */
+ public $localModel;
+ /** @var string локальный ключ */
+ public $localKey;
+ /** @var string класс внешней модели */
+ public $foreignModel;
+ /** @var string внешний ключ */
+ public $foreignKey;
+ /** @var string имя связи */
+ public $name;
+
+ /**
+ * Конструктор.
+ * @param string тип связи
+ * @param string класс локальной модели
+ * @param string класс внешней модели
+ * @param string|null локальный ключ
+ * @param string|null внешний ключ
+ */
+ public function __construct(
+ string $type, string $localModel, string $foreignModel,
+ string $foreignKey = null, string $localKey = null
+ ) {
+ if (!$foreignKey) {
+ // $foreignKey = static::generateForeignKey($localModel);
+ $foreignKey = ('belongsTo' === $type)
+ ? $foreignModel::primaryKey()
+ : static::generateForeignKey($localModel);
+ }
+ if (!$localKey) {
+ // $localKey = $localModel::primaryKey();
+ $localKey = ('belongsTo' === $type)
+ ? static::generateForeignKey($foreignModel)
+ : $localModel::primaryKey();
+ }
+ $this->type = $type;
+ $this->localModel = $localModel;
+ $this->foreignModel = $foreignModel;
+
+ $this->localKey = $localKey;
+ $this->foreignKey = $foreignKey;
+
+ $this->localTable = $localModel::tableName();
+ $this->foreignTable = $foreignModel::tableName();
+ $this->localFullKey = "$this->localTable.$this->localKey";
+ $this->foreignFullKey = "$this->foreignTable.$this->foreignKey";
+ $this->name = $this->foreignTable;
+ }
+
+ /**
+ * Генерация внешнего ключа.
+ * @param string класс локальной модели
+ * @return string сгенерированный ключ
+ */
+ protected static function generateForeignKey(string $localModel): string
+ {
+ $pk = $localModel::primaryKey();
+ if ('id' === $pk) {
+ $pk = $localModel::tableName();
+ if (($len = strlen($pk)) > 1 && strrpos($pk, 's') === $len - 1) {
+ $pk = substr($pk, 0, $len - 1);
+ }
+ $pk .= '_id';
+ }
+ return $pk;
+ }
+
+ /**
+ * Установка имени связей модели.
+ * @param string имя
+ * @return self
+ */
+ public function setName(string $name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * Получение внешнего столбца.
+ * @param string столбец
+ * @param bool|null исользовать ли имя связи вместо имение таблицы
+ * @return string
+ */
+ public function foreignColumn(string $column, bool $useName = false): string
+ {
+ return ($useName ? $this->name : $this->foreignTable) .'.'. $column;
+ }
+
+ /**
+ * Получение внешнего первичного ключа.
+ * @param bool|null исользовать ли имя связи вместо имение таблицы
+ * @return string
+ */
+ public function foreignPrimary(bool $useName = false): string
+ {
+ return $this->foreignColumn($this->foreignModel::primaryKey(), $useName);
+ }
+
+ /**
+ * Получение внешнего ключа связи.
+ * @param bool|null исользовать ли имя связи вместо имение таблицы
+ * @return string
+ */
+ public function foreignKey(bool $useName = false): string
+ {
+ return $this->foreignColumn($this->foreignKey, $useName);
+ }
+
+ /**
+ * Получение локального столбца.
+ * @param bool|null исользовать ли полный ключ
+ * @return string
+ */
+ public function localColumn(string $column, bool $useFull = true): string
+ {
+ return ($useFull ? ($this->localTable.'.') : '') . $column;
+ }
+
+ /**
+ * Получение локального ключа связи.
+ * @param bool|null исользовать ли полный ключ
+ * @return string
+ */
+ public function localKey(bool $useFull = true): string
+ {
+ return $this->localColumn($this->localKey, $useFull);
+ }
+
+ /**
+ * Получение аргументов для join.
+ * @param QueryBuilder
+ */
+ public function joinArgs(QueryBuilder $query = null, bool $useName = false): array
+ {
+ return [
+ $query ?? $this->foreignTable, $this->name,
+ $this->foreignKey($useName), $this->localKey(true)
+ ];
+ }
+
+ /**
+ * Добавление связи с моделью.
+ * @param ActiveRecord модель
+ * @param array данные внешней модели
+ * @return ActiveRecord модель
+ */
+ public function addRelated(ActiveRecord $model, array $foreignData)
+ {
+ return $model->addRelated($this->name, new $this->foreignModel($foreignData));
+ }
+
+ /**
+ * Является ли связь связью к одному.
+ * @return bool
+ */
+ public function isOne(): bool
+ {
+ return in_array($this->type, ['hasOne', 'belongsTo']);
+ }
+
+ /**
+ * Является ли связь связью ко многим.
+ * @return bool
+ */
+ public function isMany(): bool
+ {
+ return !$this->isOne();
+ }
+
+
+ /**
+ * Приведение к строке.
+ * @return string
+ */
+ public function __toString()
+ {
+ return json_encode([
+ 'localFullKey' => $this->localFullKey,
+ 'foreignFullKey' => $this->foreignFullKey
+ ], JSON_UNESCAPED_UNICODE);
+ // return json_encode(get_object_vars($this), JSON_UNESCAPED_UNICODE);
+ }
+}
diff --git a/src/RelationsMap.php b/src/RelationsMap.php
new file mode 100644
index 0000000..db996cc
--- /dev/null
+++ b/src/RelationsMap.php
@@ -0,0 +1,131 @@
+
+ */
+namespace Evas\Orm;
+
+use Evas\Base\Help\PhpHelp;
+use Evas\Orm\Exceptions\OrmException;
+use Evas\Orm\Relation;
+
+class RelationsMap
+{
+ /** @static Relation[] by models */
+ protected static $relations = [];
+
+ /**
+ * Инициализция связей класса модели.
+ * @param string класс модели
+ * @throws OrmException
+ */
+ public static function initRelations(string $modelClass)
+ {
+ $relations = $modelClass::relations();
+ if ($relations) foreach ($relations as $name => &$relation) {
+ if (!$relation instanceof Relation) {
+ throw new OrmException(sprintf(
+ 'Relation for model %s must be instance of %s, %s given',
+ $modelClass, Relation::class, PhpHelp::getType($relation)
+ ));
+ }
+ $relation->setName($name);
+ }
+ static::$relations[$modelClass] = [];
+ static::setRelations($modelClass, $relations);
+ }
+
+ /**
+ * Получение связей класса модели.
+ * @param string класс модели
+ * @return array|null
+ */
+ public static function getRelations(string $modelClass): ?array
+ {
+ $relations = static::$relations[$modelClass] ?? null;
+ if (is_null($relations)) {
+ static::initRelations($modelClass);
+ }
+ return static::$relations[$modelClass] ?? null;
+ }
+
+ /**
+ * Получение связи класса модели.
+ * @param string класс модели
+ * @param string имя связи
+ * @return array|null
+ */
+ public static function getRelation(string $modelClass, string $name): ?Relation
+ {
+ return static::getRelations($modelClass)[$name] ?? null;
+ }
+
+ /**
+ * Провеерка наличия связей класса модели.
+ * @param string класс модели
+ * @return bool
+ */
+ public static function hasRelations(string $modelClass): bool
+ {
+ return !is_null(static::getRelations($modelClass));
+ // return isset(static::$relations[$modelClass]);
+ }
+
+ /**
+ * Провеерка наличия связи класса модели.
+ * @param string класс модели
+ * @param string имя связи
+ * @return bool
+ */
+ public static function hasRelation(string $modelClass, string $name): bool
+ {
+ return !is_null(static::getRelation($modelClass, $name));
+ }
+
+ /**
+ * Установка связей класса модели.
+ * @param string класс модели
+ * @param Relation[] связи
+ */
+ public static function setRelations(string $modelClass, array $relations)
+ {
+ foreach ($relations as $relation) {
+ static::setRelation($modelClass, $relation);
+ }
+ }
+
+ /**
+ * Установка связи класса модели.
+ * @param string класс модели
+ * @param Relation связь
+ */
+ public static function setRelation(string $modelClass, Relation $relation)
+ {
+ if (!static::hasRelations($modelClass)) {
+ static::$relations[$modelClass] = [];
+ }
+ static::$relations[$modelClass][$relation->name] = &$relation;
+ }
+
+ /**
+ * Удаление связей класса модели или всех связей.
+ * @param string|null класс модели
+ */
+ public static function unsetRelations(string $modelClass = null)
+ {
+ if (is_null($modelClass)) unset(static::$relations);
+ else unset(static::$relations[$modelClass]);
+ }
+
+ /**
+ * Удаление связи класса модели.
+ * @param string класс модели
+ * @param string имя связи
+ */
+ public static function unsetRelation(string $modelClass, string $name)
+ {
+ unset(static::$relations[$modelClass][$name]);
+ }
+}
diff --git a/src/Traits/ActiveRecordConvertTrait.php b/src/Traits/ActiveRecordConvertTrait.php
new file mode 100644
index 0000000..4c563f9
--- /dev/null
+++ b/src/Traits/ActiveRecordConvertTrait.php
@@ -0,0 +1,48 @@
+
+ */
+namespace Evas\Orm\Traits;
+
+trait ActiveRecordConvertTrait
+{
+ /**
+ * Конвертация данных и связей модели в массив.
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return $this->getData();
+ // return array_merge($this->getData(), $this->getRelatedCollections());
+ }
+
+ /**
+ * Конвертация модели в JSON сериализацию.
+ * @return array
+ */
+ public function jsonSerialize(): array
+ {
+ return $this->toArray();
+ }
+
+ /**
+ * Конвертация данных и связей модели в JSON.
+ * @param int|null опции для json_encode
+ * @return string
+ */
+ public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
+ {
+ return json_encode($this->jsonSerialize(), $options);
+ }
+
+ /**
+ * Конвертация модели в строку.
+ * @return string
+ */
+ public function __toString(): string
+ {
+ return $this->toJson();
+ }
+}
diff --git a/src/Traits/ActiveRecordDataTrait.php b/src/Traits/ActiveRecordDataTrait.php
new file mode 100644
index 0000000..6ccebe5
--- /dev/null
+++ b/src/Traits/ActiveRecordDataTrait.php
@@ -0,0 +1,96 @@
+
+ */
+namespace Evas\Orm\Traits;
+
+trait ActiveRecordDataTrait
+{
+ /** @var array данные модели */
+ protected $modelData = [];
+
+ /**
+ * Дефолтные значения новой записи.
+ * @return array
+ */
+ public static function default(): array
+ {
+ return [];
+ }
+
+ /**
+ * Получение значения первичного ключа модели.
+ * @return int|string|null
+ */
+ public function primaryValue()
+ {
+ return $this->{static::primaryKey()};
+ }
+
+ /**
+ * Заполнение модели свойствами.
+ * @param array свойства модели
+ * @return self
+ */
+ public function fill(array $props)
+ {
+ foreach ($props as $name => $value) {
+ $this->$name = $value;
+ }
+ return $this;
+ }
+
+ /**
+ * Получение данных модели.
+ * @return array
+ */
+ public function getData(): array
+ {
+ return $this->modelData;
+ }
+
+ /**
+ * Установка свойства.
+ * @param string имя
+ * @param mixed значение
+ */
+ public function __set(string $name, $value)
+ {
+ $this->modelData[$name] = $value;
+ }
+
+ /**
+ * Получение свойства или связанных моделей.
+ * @param string имя
+ * @return mixed значение
+ */
+ public function __get(string $name)
+ {
+ if (static::hasRelation($name)) {
+ return $this->getRelatedCollection($name);
+ } else {
+ return $this->modelData[$name] ?? null;
+ }
+ }
+
+ /**
+ * Проверка наличия свойства.
+ * @param string имя свойства
+ * @return bool
+ */
+ public function __isset(string $name): bool
+ {
+ return isset($this->modelData[$name]);
+ }
+
+ /**
+ * Очистка свойства.
+ * @param string имя свойства
+ */
+ public function __unset(string $name)
+ {
+ unset($this->modelData[$name]);
+ }
+}
diff --git a/src/Traits/ActiveRecordDatabaseTrait.php b/src/Traits/ActiveRecordDatabaseTrait.php
new file mode 100644
index 0000000..59ef4b9
--- /dev/null
+++ b/src/Traits/ActiveRecordDatabaseTrait.php
@@ -0,0 +1,40 @@
+
+ */
+namespace Evas\Orm\Traits;
+
+use Evas\Base\App;
+use Evas\Db\Interfaces\DatabaseInterface;
+
+trait ActiveRecordDatabaseTrait
+{
+ /** @var string имя соединения с базой данных */
+ public static $dbname;
+ /** @var string имя соединения с базой данных только для записи */
+ public static $dbnameWrite;
+
+ /**
+ * Получение соединения с базой данных.
+ * @param bool использовать ли соединение для записи
+ * @return DatabaseInterface
+ */
+ public static function db(bool $write = false): DatabaseInterface
+ {
+ $dbname = static::dbName($write);
+ return App::db($dbname);
+ }
+
+ /**
+ * Получение имени соединения с базой данных.
+ * @param bool использовать ли соединение для записи
+ * @return string|null
+ */
+ public static function dbName(bool $write = false): ?string
+ {
+ return true === $write && !empty(static::$dbnameWrite)
+ ? static::$dbnameWrite : static::$dbname;
+ }
+}
diff --git a/src/Traits/ActiveRecordIdentityTrait.php b/src/Traits/ActiveRecordIdentityTrait.php
new file mode 100644
index 0000000..bf31929
--- /dev/null
+++ b/src/Traits/ActiveRecordIdentityTrait.php
@@ -0,0 +1,45 @@
+
+ */
+namespace Evas\Orm\Traits;
+
+use Evas\Orm\ActiveRecord;
+use Evas\Orm\IdentityMap;
+
+trait ActiveRecordIdentityTrait
+{
+ /** @var string идентификатор модели для IdentityMap */
+ protected $identity;
+
+ /**
+ * Получение идентификатора модели для IdentityMap
+ * @return string идентификатор модели для IdentityMap
+ * */
+ public function identity(): string
+ {
+ if (!$this->identity) {
+ $this->identity = implode(':', [static::class, $this->primaryValue(), static::dbName()]);
+ }
+ return $this->identity;
+ }
+
+ /**
+ * Сохранение модели в IdentityMap.
+ * @return static
+ */
+ public function identityMapSave(): ActiveRecord
+ {
+ return IdentityMap::getWithSave($this);
+ }
+
+ /**
+ * Удаление модели из IdentityMap.
+ */
+ public function identityMapRemove()
+ {
+ IdentityMap::unset($this);
+ }
+}
diff --git a/src/Traits/ActiveRecordQueryTrait.php b/src/Traits/ActiveRecordQueryTrait.php
new file mode 100644
index 0000000..f963517
--- /dev/null
+++ b/src/Traits/ActiveRecordQueryTrait.php
@@ -0,0 +1,56 @@
+
+ */
+namespace Evas\Orm\Traits;
+
+use Evas\Orm\QueryBuilder;
+
+trait ActiveRecordQueryTrait
+{
+ /**
+ * Поиск по sql-запросу.
+ * @param string sql-запрос
+ * @param array|null значения запроса для экранирования
+ * @return static|static[]
+ */
+ public static function query(string $sql, array $values = null)
+ {
+ $qr = static::db()->query($sql, $values);
+ return strstr($qr->stmt()->queryString, 'LIMIT 1')
+ ? $qr->object(static::class)
+ : $qr->objectAll(static::class);
+ }
+
+ /**
+ * Проброс сборщика запросов через магический вызов статического метода.
+ * @param string имя метода
+ * @param array|null аргументы
+ * @return mixed результат выполнения метода
+ * @throws \BadMethodCallException
+ */
+ public static function __callStatic(string $name, array $args = null)
+ {
+ if (method_exists(QueryBuilder::class, $name)) {
+ $db = static::db();
+ return (new QueryBuilder($db, static::class))->$name(...$args);
+ }
+ throw new \BadMethodCallException(sprintf(
+ 'Call to undefined method %s::%s()', __CLASS__, $name
+ ));
+ }
+
+ /**
+ * Поиск записи/записей.
+ * @param array|int столбец или столбцы
+ * @return static|static[]
+ */
+ public static function find($ids)
+ {
+ $ids = func_num_args() > 1 ? func_get_args() : $ids;
+ $db = static::db();
+ return (new QueryBuilder($db, static::class))->find($ids);
+ }
+}
diff --git a/src/Traits/ActiveRecordRelatedsTrait.php b/src/Traits/ActiveRecordRelatedsTrait.php
new file mode 100644
index 0000000..dcc99d7
--- /dev/null
+++ b/src/Traits/ActiveRecordRelatedsTrait.php
@@ -0,0 +1,90 @@
+
+ */
+namespace Evas\Orm\Traits;
+
+use Evas\Orm\ActiveRecord;
+use Evas\Orm\RelatedCollection;
+
+trait ActiveRecordRelatedsTrait
+{
+ /** @var array связанные коллекции моделей */
+ protected $relatedCollections = [];
+
+ /**
+ * Установка связанной коллекции модели.
+ * @param string имя связи
+ * @param array связанные модели
+ * @return self
+ */
+ public function setRelatedCollection(string $name, array $relateds)
+ {
+ unset($this->relatedCollections[$name]);
+ foreach ($relateds as &$related) {
+ $this->addRelated($name, $related);
+ }
+ return $this;
+ }
+
+ /**
+ * Добавление связанной модели в коллекцию.
+ * @param string имя связи
+ * @param ActiveRecord модель
+ * @return self
+ */
+ public function addRelated(string $name, ActiveRecord $related)
+ {
+ $relation = static::getRelation($name);
+ $related = $related->identityMapSave();
+ if (!$relation) return $this;
+ // if (false) {
+ if ($relation->isOne()) {
+ if (!isset($this->relatedCollections[$name]) && $related->primaryValue()) {
+ $this->relatedCollections[$name] = &$related;
+ }
+ return $this;
+ }
+ if (!isset($this->relatedCollections[$name])) {
+ $this->relatedCollections[$name] = new RelatedCollection($this, static::getRelation($name));
+ }
+ if ($related->primaryValue()) {
+ $this->relatedCollections[$name]->add($related);
+ }
+ return $this;
+ }
+
+ public function addRelatedData(string $name, array $data)
+ {
+ $relation = static::getRelation($name);
+ if (!$relation) return $this;
+ return $this->addRelated($name, new $relation->foreignModel($data));
+ }
+
+ /**
+ * Получение всех связанных записей модели.
+ * @return array
+ */
+ public function getRelatedCollections(): array
+ {
+ return $this->relatedCollections;
+ }
+
+ /**
+ * Получение конкретных связанных записей модели.
+ * @param string имя связи
+ * @return RelatedCollection|null
+ */
+ public function getRelatedCollection(string $name)//: ?RelatedCollection
+ {
+ if (static::hasRelation($name)) {
+ if (!isset($this->relatedCollections[$name])) {
+ $this->loadRelated($name);
+ }
+ return $this->relatedCollections[$name] ?? null;
+ }
+ return null;
+ }
+}
diff --git a/src/Traits/ActiveRecordRelationsTrait.php b/src/Traits/ActiveRecordRelationsTrait.php
new file mode 100644
index 0000000..ffbd620
--- /dev/null
+++ b/src/Traits/ActiveRecordRelationsTrait.php
@@ -0,0 +1,90 @@
+
+ */
+namespace Evas\Orm\Traits;
+
+use Evas\Base\Help\PhpHelp;
+use Evas\Orm\Relation;
+use Evas\Orm\RelationsMap;
+
+trait ActiveRecordRelationsTrait
+{
+ /**
+ * Описание связей модели.
+ * @return array
+ */
+ public static function relations(): array
+ {
+ return [];
+ }
+
+
+ /**
+ * Установка множественной связи.
+ * @param string класс внешней модели
+ * @param string|null внешний ключ
+ * @param string|null локальный ключ
+ * @return Relation
+ */
+ public static function hasMany(
+ string $foreignModel, string $foreignKey = null, string $localKey = null
+ ): Relation {
+ return new Relation(
+ 'hasMany', static::class, $foreignModel, $foreignKey, $localKey
+ );
+ }
+
+ /**
+ * Установка единичной связи.
+ * @param string класс внешней модели
+ * @param string|null внешний ключ
+ * @param string|null локальный ключ
+ * @return Relation
+ */
+ public static function hasOne(
+ string $foreignModel, string $foreignKey = null, string $localKey = null
+ ): Relation {
+ return new Relation(
+ 'hasOne', static::class, $foreignModel, $foreignKey, $localKey
+ );
+ }
+
+ /**
+ * Установка единичной связи к родителю.
+ * @param string класс внешней модели
+ * @param string|null внешний ключ
+ * @param string|null локальный ключ
+ * @return Relation
+ */
+ public static function belongsTo(
+ string $foreignModel, string $foreignKey = null, string $localKey = null
+ ): Relation {
+ return new Relation(
+ 'belongsTo', static::class, $foreignModel, $foreignKey, $localKey
+ );
+ }
+
+
+ /**
+ * Получение связи по имени.
+ * @param string имя связи
+ * @return Relation|null
+ */
+ public static function getRelation(string $name): ?Relation
+ {
+ return RelationsMap::getRelation(static::class, $name);
+ }
+
+ /**
+ * Проверка наличия связи.
+ * @param string имя связи
+ * @return bool
+ */
+ public static function hasRelation(string $name): bool
+ {
+ return RelationsMap::hasRelation(static::class, $name);
+ }
+}
diff --git a/src/Traits/ActiveRecordStateTrait.php b/src/Traits/ActiveRecordStateTrait.php
new file mode 100644
index 0000000..d7abcb9
--- /dev/null
+++ b/src/Traits/ActiveRecordStateTrait.php
@@ -0,0 +1,85 @@
+
+ */
+namespace Evas\Orm\Traits;
+
+trait ActiveRecordStateTrait
+{
+ /** @var array состояние полей модели */
+ protected $state = [];
+
+ /**
+ * Сохранение состояния.
+ * @return self
+ */
+ public function saveState()
+ {
+ $this->state = $this->getProps();
+ return $this;
+ }
+
+ /**
+ * Получение состояния сохраняемых полей.
+ * @return array
+ */
+ public function getState(): array
+ {
+ return $this->state;
+ }
+
+ /**
+ * Проверка наличия значения первичного ключа записи в состоянии.
+ * @return bool
+ */
+ public function isStateHasPrimaryValue(): bool
+ {
+ return isset($this->state[static::primaryKey()]);
+ }
+
+ /**
+ * Получение данных записи для базы данных.
+ * @return array
+ */
+ public function getProps(): array
+ {
+ $props = [];
+ foreach (static::columns() as &$column) {
+ if (isset($this->$column)) $props[$column] = $this->$column;
+ }
+ return $props;
+ }
+
+ /**
+ * Получение маппинга измененных свойств записи.
+ * @return array
+ */
+ public function getUpdatedProps(): array
+ {
+ $props = $this->getProps();
+ if (empty($this->primaryValue())) {
+ return $props;
+ } else {
+ $state = $this->getState();
+ // // return array_diff($props, $state ?? []);
+ // // var_dump($state);
+ // // echo '
';
+ // // var_dump($props);
+ // // echo '
';
+ $updated = [];
+ foreach ($props as $name => $value) {
+ if (!isset($state[$name]) || $value !== $state[$name]) {
+ $updated[$name] = $value;
+ }
+ }
+ // var_dump($updated); echo '